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:
+5
-5
@@ -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/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 |
|
| `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) |
|
| `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/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 |
|
| `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/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/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 |
|
| `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/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`, `StringTokenizer` | neu nach Java; die Groß-/Kleinschreibung gegenüber ASCII am JDK über die ganze BMP gemessen |
|
| `src/javaFileSystem.ts`, `src/javaString.ts` | Java `Dateisystem.holeDatei`, `String.trim`/`split`/`equalsIgnoreCase` | neu nach Java; die Groß-/Kleinschreibung gegenüber ASCII am JDK über die ganze BMP gemessen |
|
||||||
| `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/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/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/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 |
|
| `test/pathFolding.test.ts`, `test/path-folding.*.json` | — | neu: welche Pfade dieselbe Datei meinen, Ergebnis aus dem Swift-Kern |
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
// Java's DNS server keeps its records in the text file /dns/hosts and reads
|
// Java's DNS server keeps its records in the text file /dns/hosts and reads
|
||||||
// them on every query (DNSServer.leseRecordListe, ResourceRecord(String)).
|
// them on every query (DNSServer.leseRecordListe, ResourceRecord(String)).
|
||||||
// Measured on test/dns-hosts.cases.txt against Java (dns-hosts.java.json)
|
// 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;
|
// - 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;
|
// - there are no comments, no "IN" and no zone-file order;
|
||||||
// - a TTL that is not a Java int silently becomes 3600;
|
// - a TTL that is not a number silently becomes 3600;
|
||||||
// - a name Java does not accept becomes the empty name, and the record
|
// - a name that is not a domain name becomes the empty name, and the record
|
||||||
// still counts - it just never answers, like any record whose type is
|
// still counts - it just never answers, like one of another type;
|
||||||
// not exactly A, MX or NS (lower-case "a" included);
|
|
||||||
// - duplicates stay.
|
// - 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 type { DnsResourceRecord } from '@mdg-filius/core';
|
||||||
import { javaFileContent } from './javaFileSystem.js';
|
import { javaFileContent } from './javaFileSystem.js';
|
||||||
@@ -16,8 +20,8 @@ import { javaIpCheck, javaParseInt } from './javaIp.js';
|
|||||||
import { javaTrim } from './javaString.js';
|
import { javaTrim } from './javaString.js';
|
||||||
import type { FlsFileTree } from './systemSoftware.js';
|
import type { FlsFileTree } from './systemSoftware.js';
|
||||||
|
|
||||||
/** A record as Java's ResourceRecord holds it (the getters). */
|
/** A record of /dns/hosts, as Java's ResourceRecord holds it (the getters). */
|
||||||
export interface JavaResourceRecord {
|
export interface HostsRecord {
|
||||||
readonly domainName: string;
|
readonly domainName: string;
|
||||||
readonly type: string;
|
readonly type: string;
|
||||||
readonly ttl: number;
|
readonly ttl: number;
|
||||||
@@ -26,10 +30,22 @@ export interface JavaResourceRecord {
|
|||||||
|
|
||||||
export const DNS_HOSTS_PATH = '/dns/hosts';
|
export const DNS_HOSTS_PATH = '/dns/hosts';
|
||||||
|
|
||||||
// EingabenUeberpruefung.musterDomain, including its quirks: labels start with
|
// Labels of letters, digits and inner hyphens, an optional final dot; the
|
||||||
// a letter, labels after the first may hold a backslash, and the empty
|
// empty string counts too.
|
||||||
// string is a valid domain.
|
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])?)*)?\.?$/;
|
||||||
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])?)*)?\.?$/;
|
/**
|
||||||
|
* 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.
|
// EingabenUeberpruefung.musterIpAdresse.
|
||||||
const JAVA_IP_ADDRESS =
|
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]))$/;
|
/^(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);
|
return javaFileContent(root, DNS_HOSTS_PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ANSWERED_TYPES = new Set(['A', 'MX', 'NS']);
|
||||||
|
|
||||||
/** `DNSServer.leseRecordListe` on the content of /dns/hosts. */
|
/** `DNSServer.leseRecordListe` on the content of /dns/hosts. */
|
||||||
export function readJavaDnsHosts(content: string): JavaResourceRecord[] {
|
export function readDnsHosts(content: string): HostsRecord[] {
|
||||||
const records: JavaResourceRecord[] = [];
|
const records: HostsRecord[] = [];
|
||||||
for (const token of content.split('\n')) {
|
for (const token of content.split('\n')) {
|
||||||
const line = javaTrim(token);
|
const line = javaTrim(token);
|
||||||
// line.split(" ", 5).length < 4: fewer than three single spaces.
|
// line.split(" ", 5).length < 4: fewer than three single spaces.
|
||||||
if (line === '' || line.split(' ').length < 4) continue;
|
if (line === '' || line.split(' ').length < 4) continue;
|
||||||
const record = javaResourceRecord(line);
|
const record = hostsRecord(line);
|
||||||
if (isValidJavaRecord(record)) records.push(record);
|
if (isValidRecord(record)) records.push(record);
|
||||||
}
|
}
|
||||||
return records;
|
return records;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** `new ResourceRecord(line)`: up to four space-separated fields. */
|
/** `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);
|
const fields = line.split(' ').filter((field) => field !== '').map(javaTrim);
|
||||||
let domainName = '';
|
let domainName = '';
|
||||||
let type = 'A';
|
let type = 'A';
|
||||||
let ttl = 3600;
|
let ttl = 3600;
|
||||||
let rdata = '';
|
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]}.`;
|
domainName = fields[0].endsWith('.') ? fields[0] : `${fields[0]}.`;
|
||||||
}
|
}
|
||||||
if (fields[1] !== undefined) type = fields[1];
|
if (fields[1] !== undefined) {
|
||||||
if (fields[2] !== undefined) ttl = javaParseInt(fields[2]) ?? ttl;
|
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) {
|
if (fields[3] !== undefined) {
|
||||||
// Java tests ".*\." (no line terminator before the dot); a target with
|
// Java tests ".*\." (no line terminator before the dot); a target with
|
||||||
// one fails the domain check anyway, so endsWith says the same.
|
// 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 };
|
return { domainName, type, ttl, rdata };
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidJavaRecord(record: JavaResourceRecord): boolean {
|
function isValidRecord(record: HostsRecord): boolean {
|
||||||
switch (record.type) {
|
switch (record.type) {
|
||||||
case 'A':
|
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 'MX':
|
||||||
case 'NS':
|
case 'NS':
|
||||||
return JAVA_DOMAIN.test(record.domainName) && JAVA_DOMAIN.test(record.rdata);
|
return isDomainName(record.domainName) && isDomainName(record.rdata);
|
||||||
default:
|
default:
|
||||||
return true;
|
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
|
* 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
|
* type (A, MX, NS) and by name ignoring case, so a record with the empty
|
||||||
* the empty name or another type never answers and is left out. The file
|
* name or another type never answers and is left out. The file itself stays
|
||||||
* itself stays as it is.
|
* 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[] = [];
|
const answered: DnsResourceRecord[] = [];
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
if (record.domainName === '') continue;
|
if (record.domainName === '') continue;
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ import {
|
|||||||
type TopologyNode,
|
type TopologyNode,
|
||||||
type VirtualFileContent,
|
type VirtualFileContent,
|
||||||
} from '@mdg-filius/core';
|
} 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 { FlsApplicationClass, FlsFileTree, FlsFirewall } from './systemSoftware.js';
|
||||||
import type { FlsDocumentationItem, FlsInterface, FlsNode, FlsNodeClass, FlsTopology } from './topology.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 primary = trimmedOrEmpty(source.interfaces[0]?.ip);
|
||||||
const forwarding = recursive && forwarder !== '' && forwarder !== primary;
|
const forwarding = recursive && forwarder !== '' && forwarder !== primary;
|
||||||
const hosts = javaHostsFileContent(source.system.fileSystem);
|
const hosts = javaHostsFileContent(source.system.fileSystem);
|
||||||
const records = dnsRecordsOf(readJavaDnsHosts(hosts ?? ''), (message) =>
|
const records = dnsRecordsOf(readDnsHosts(hosts ?? ''), (message) =>
|
||||||
warnings.push(`node '${node.displayName}': ${message}`),
|
warnings.push(`node '${node.displayName}': ${message}`),
|
||||||
);
|
);
|
||||||
system.dnsServerConfigurations.set(
|
system.dnsServerConfigurations.set(
|
||||||
|
|||||||
@@ -7,10 +7,12 @@
|
|||||||
// the bean list, and /konten.txt, the client's account, read (laden) when
|
// 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
|
// its window opens. The client's account is in no bean: its accessors
|
||||||
// (holeKontoListe, setzeKontoListe) are not bean properties.
|
// (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 { 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. */
|
/** AddressEntry: a display name, an address, or both. */
|
||||||
export interface JavaAddressEntry {
|
export interface JavaAddressEntry {
|
||||||
@@ -63,11 +65,6 @@ export interface JavaContact {
|
|||||||
readonly phone: string | null;
|
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_SERVER_FILE = '/mailserver/konten.txt';
|
||||||
export const EMAIL_CLIENT_FILE = '/konten.txt';
|
export const EMAIL_CLIENT_FILE = '/konten.txt';
|
||||||
|
|
||||||
@@ -213,17 +210,25 @@ function addressEntries(raw: string): JavaAddressEntry[] {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* `EmailServer.stringZuListeBenutzerkonten`: one account a line —
|
* `EmailServer.stringZuListeBenutzerkonten`: one account a line —
|
||||||
* user;domain;password;last name;first name;messages — split by a
|
* user;domain;password;last name;first name;messages. Messages are
|
||||||
* StringTokenizer, which skips empty fields; an account with fewer than five
|
* "#"-separated, seven "$"-separated fields each: sender, to, cc, bcc, date,
|
||||||
* fields makes Java throw. Messages are "#"-separated, seven "$"-separated
|
* subject, text. Java's errors are not copied (each named in
|
||||||
* fields each: sender, to, cc, bcc, date, subject, text. Java's split drops
|
* javaEmail.test.ts): its StringTokenizer skips empty fields, so an empty
|
||||||
* trailing empty fields, so a message with an empty text is lost.
|
* 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[] = [];
|
const accounts: JavaEmailAccount[] = [];
|
||||||
for (const line of javaTokens(content, '\n')) {
|
lines(content).forEach((line, index) => {
|
||||||
const fields = javaTokens(line, ';');
|
if (line === '') return;
|
||||||
if (fields.length < 5) return { exception: 'NoSuchElementException' };
|
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?];
|
const [username, , password, lastName, firstName, messages] = fields as [string, string, string, string, string, string?];
|
||||||
accounts.push({
|
accounts.push({
|
||||||
username,
|
username,
|
||||||
@@ -235,16 +240,20 @@ export function readJavaEmailServerFile(content: string): JavaEmailAccount[] | J
|
|||||||
pop3Port: null,
|
pop3Port: null,
|
||||||
smtpServer: null,
|
smtpServer: null,
|
||||||
smtpPort: null,
|
smtpPort: null,
|
||||||
messages: messages === undefined ? [] : fileMessages(messages),
|
messages: messages === undefined ? [] : fileMessages(messages, username, warn),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
return accounts;
|
return accounts;
|
||||||
}
|
}
|
||||||
|
|
||||||
function fileMessages(messages: string): JavaEmail[] {
|
function fileMessages(messages: string, username: string, warn: Warn): JavaEmail[] {
|
||||||
return javaSplit(messages, '#').flatMap((message) => {
|
return messages.split('#').flatMap((message) => {
|
||||||
const fields = javaSplit(message, '$');
|
if (message === '') return [];
|
||||||
if (fields.length !== 7) 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];
|
const [sender, to, cc, bcc, dateReceived, subject, body] = fields as [string, string, string, string, string, string, string];
|
||||||
return [
|
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 "|". */
|
/** brtonl, then undoSpecialChar: "|" is a line break, "&&00124&&" a "|". */
|
||||||
function unescapedBody(body: string): string {
|
function unescapedBody(body: string): string {
|
||||||
return body
|
return body
|
||||||
@@ -274,22 +294,25 @@ function unescapedBody(body: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `EmailAnwendung.laden`: every line makes an account, the last one stays.
|
* `EmailAnwendung.laden`: an account of nine ";"-separated fields — POP3
|
||||||
* `new EmailKonto(line)` takes nine ";"-separated fields — POP3 server, SMTP
|
* server, SMTP server, POP3 port, SMTP port, user, password, last name,
|
||||||
* server, POP3 port, SMTP port, user, password, last name, first name,
|
* first name, address; further fields are ignored. Java takes the last line
|
||||||
* address — and with fewer keeps none (trailing empty fields do not count).
|
* whatever it holds; its errors are not copied (each named in
|
||||||
* Null when the file has no line at all.
|
* 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 {
|
export function readEmailClientFile(content: string, warn: Warn): JavaEmailAccount | null {
|
||||||
const lines = javaSplit(content, '\n');
|
let account: JavaEmailAccount | null = null;
|
||||||
const line = lines[lines.length - 1];
|
lines(content).forEach((line, index) => {
|
||||||
if (line === undefined) return null;
|
const fields = line.split(';');
|
||||||
const fields = javaSplit(line, ';');
|
if (fields.length < 9) {
|
||||||
const empty = { username: null, password: null, lastName: '', firstName: '', emailAddress: null };
|
if (javaTrim(line) !== '') warn(`${EMAIL_CLIENT_FILE}: line ${index + 1} is no account of nine fields, ignored`);
|
||||||
const servers = { pop3Server: null, pop3Port: null, smtpServer: null, smtpPort: null };
|
return;
|
||||||
if (fields.length <= 8) return { ...empty, ...servers, messages: [] };
|
}
|
||||||
const [pop3Server, smtpServer, pop3Port, smtpPort, username, password, lastName, firstName, emailAddress] = fields as string[];
|
const [pop3Server, smtpServer, pop3Port, smtpPort, username, password, lastName, firstName, emailAddress] = fields as string[];
|
||||||
return {
|
account = {
|
||||||
username: username!,
|
username: username!,
|
||||||
password: password!,
|
password: password!,
|
||||||
lastName: lastName!,
|
lastName: lastName!,
|
||||||
@@ -301,4 +324,6 @@ export function readJavaEmailClientFile(content: string): JavaEmailAccount | nul
|
|||||||
smtpPort: smtpPort!,
|
smtpPort: smtpPort!,
|
||||||
messages: [],
|
messages: [],
|
||||||
};
|
};
|
||||||
|
});
|
||||||
|
return account;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// java.lang.String and java.util.StringTokenizer as FILIUS's text formats
|
// java.lang.String as FILIUS's text formats use it - each differs from its
|
||||||
// use them — each differs from its JavaScript namesake.
|
// JavaScript namesake.
|
||||||
|
|
||||||
/** `String.trim()`: every UTF-16 unit up to U+0020 off both ends. */
|
/** `String.trim()`: every UTF-16 unit up to U+0020 off both ends. */
|
||||||
export function javaTrim(text: string): string {
|
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();
|
while (parts.length > 0 && parts[parts.length - 1] === '') parts.pop();
|
||||||
return parts;
|
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 !== '');
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -27,19 +27,18 @@
|
|||||||
// not know Java's constructors and sees a placeholder per path; they are
|
// not know Java's constructors and sees a placeholder per path; they are
|
||||||
// merged here.
|
// merged here.
|
||||||
|
|
||||||
import { JAVA_DOMAIN } from './dnsHosts.js';
|
import { isDomainName } from './dnsHosts.js';
|
||||||
import {
|
import {
|
||||||
EMAIL_CLIENT_FILE,
|
EMAIL_CLIENT_FILE,
|
||||||
EMAIL_SERVER_FILE,
|
EMAIL_SERVER_FILE,
|
||||||
javaContacts,
|
javaContacts,
|
||||||
javaEmailAccounts,
|
javaEmailAccounts,
|
||||||
javaEmails,
|
javaEmails,
|
||||||
readJavaEmailClientFile,
|
readEmailClientFile,
|
||||||
readJavaEmailServerFile,
|
readEmailServerFile,
|
||||||
type JavaContact,
|
type JavaContact,
|
||||||
type JavaEmail,
|
type JavaEmail,
|
||||||
type JavaEmailAccount,
|
type JavaEmailAccount,
|
||||||
type JavaFailure,
|
|
||||||
} from './javaEmail.js';
|
} from './javaEmail.js';
|
||||||
import { javaFileContent } from './javaFileSystem.js';
|
import { javaFileContent } from './javaFileSystem.js';
|
||||||
import {
|
import {
|
||||||
@@ -151,16 +150,16 @@ export interface FlsApplication {
|
|||||||
maxPeers?: number | null;
|
maxPeers?: number | null;
|
||||||
/** Firewall: the personal firewall. */
|
/** Firewall: the personal firewall. */
|
||||||
firewall?: FlsFirewall;
|
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;
|
mailDomain?: string;
|
||||||
/** EmailServer: the accounts of the bean list. */
|
/** EmailServer: the accounts of the bean list. */
|
||||||
beanAccounts?: JavaEmailAccount[];
|
beanAccounts?: JavaEmailAccount[];
|
||||||
/**
|
/**
|
||||||
* EmailServer: the accounts of /mailserver/konten.txt, which Java reads at
|
* EmailServer: the accounts of /mailserver/konten.txt, which Java reads at
|
||||||
* every simulation start — null without the file (Java then starts with
|
* 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). */
|
/** EmailAnwendung: its folders (Java's empfangene, gesendete, erstellte Nachrichten). */
|
||||||
received?: JavaEmail[];
|
received?: JavaEmail[];
|
||||||
sent?: JavaEmail[];
|
sent?: JavaEmail[];
|
||||||
@@ -313,10 +312,10 @@ function applications(map: JavaValue | undefined, root: FlsFileTree, warn: Warn)
|
|||||||
case 'filius.software.email.EmailServer': {
|
case 'filius.software.email.EmailServer': {
|
||||||
entry.isActive = boolean(at('aktiv'), false, 'EmailServer aktiv', warn);
|
entry.isActive = boolean(at('aktiv'), false, 'EmailServer aktiv', warn);
|
||||||
const domain = optionalString(at('mailDomain'), 'EmailServer mailDomain', 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);
|
entry.beanAccounts = javaEmailAccounts(at('listeBenutzerkonten'), warn);
|
||||||
const file = javaFileContent(root, EMAIL_SERVER_FILE);
|
const file = javaFileContent(root, EMAIL_SERVER_FILE);
|
||||||
entry.fileAccounts = file === undefined ? null : readJavaEmailServerFile(file);
|
entry.fileAccounts = file === undefined ? null : readEmailServerFile(file, warn);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'filius.software.email.EmailAnwendung': {
|
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.drafts = javaEmails(at('erstellteNachrichten'), warn);
|
||||||
entry.addressBook = javaContacts(at('adressbuch'), warn);
|
entry.addressBook = javaContacts(at('adressbuch'), warn);
|
||||||
const file = javaFileContent(root, EMAIL_CLIENT_FILE);
|
const file = javaFileContent(root, EMAIL_CLIENT_FILE);
|
||||||
entry.fileAccount = file === undefined ? null : readJavaEmailClientFile(file);
|
entry.fileAccount = file === undefined ? null : readEmailClientFile(file, warn);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -406,16 +405,17 @@ function dhcpServer(server: JavaValue | undefined, warn: Warn): FlsDhcpServer {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* setStaticAssignedAddresses: each entry split at whitespace (StringUtils.split),
|
* setStaticAssignedAddresses: each entry split at whitespace (StringUtils.split),
|
||||||
* MAC and IP from the first two parts. An entry with fewer throws inside the
|
* MAC and IP from the first two parts. An entry with fewer makes Java throw
|
||||||
* loop — XMLDecoder swallows it, and the entries after it are lost.
|
* 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 }[] {
|
function staticAssignments(list: JavaValue | undefined, warn: Warn): { mac: string; ip: string }[] {
|
||||||
const result: { mac: string; ip: string }[] = [];
|
const result: { mac: string; ip: string }[] = [];
|
||||||
for (const entry of listItems(list)) {
|
for (const entry of listItems(list)) {
|
||||||
const parts = typeof entry === 'string' ? javaWhitespaceSplit(entry) : [];
|
const parts = typeof entry === 'string' ? javaWhitespaceSplit(entry) : [];
|
||||||
if (parts.length < 2) {
|
if (parts.length < 2) {
|
||||||
warn(`DHCPServer: static assignment ${describe(entry)} has no MAC and IP; Java stops reading here`);
|
warn(`DHCPServer: static assignment ${describe(entry)} has no MAC and IP, left out`);
|
||||||
break;
|
continue;
|
||||||
}
|
}
|
||||||
result.push({ mac: parts[0]!, ip: parts[1]! });
|
result.push({ mac: parts[0]!, ip: parts[1]! });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
// Java's reading of /dns/hosts (DNSServer.leseRecordListe): every case in
|
// 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
|
// 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
|
// 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
|
// reader must match Java except where Java errs (JAVA_BUGS); the records the
|
||||||
// Swift except where Java answers differently — each case named below.
|
// model keeps must match Swift except where Java's format says otherwise —
|
||||||
|
// each case named below.
|
||||||
|
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import { dirname, join } from 'node:path';
|
import { dirname, join } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { describe, expect, it } from 'vitest';
|
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));
|
const here = dirname(fileURLToPath(import.meta.url));
|
||||||
type Row = [string, string, number, string];
|
type Row = [string, string, number, string];
|
||||||
@@ -21,7 +22,7 @@ function unescape(line: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function modelRows(line: string, warnings: string[] = []): Row[] {
|
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.name,
|
||||||
record.type,
|
record.type,
|
||||||
record.ttlSeconds,
|
record.ttlSeconds,
|
||||||
@@ -31,13 +32,6 @@ function modelRows(line: string, warnings: string[] = []): Row[] {
|
|||||||
|
|
||||||
/** Lines Swift reads as records that Java never answers. */
|
/** Lines Swift reads as records that Java never answers. */
|
||||||
const SWIFT_READS_JAVA_NEVER_ANSWERS: { reason: string; lines: string[] }[] = [
|
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',
|
reason: 'Java splits fields at single spaces only: a tab or NBSP is part of a field',
|
||||||
lines: [
|
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',
|
reason: 'two final dots make the name empty in Java',
|
||||||
lines: [
|
lines: [String.raw`www.filius.de.. A 3600 1.2.3.4\u000A`],
|
||||||
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: 'an MX or NS target must be a name Java accepts: no preference, no address, one final dot',
|
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. */
|
/** Lines Java answers that Swift drops or reads otherwise. */
|
||||||
const JAVA_ANSWERS_SWIFT_DROPS: { reason: string; lines: string[] }[] = [
|
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: [
|
lines: [
|
||||||
String.raw`www.filius.de A x 1.2.3.4\u000A`,
|
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 4294967295 1.2.3.4\u000A`,
|
||||||
String.raw`www.filius.de A 99999999999 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`,
|
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 -5 1.2.3.4\u000A`,
|
||||||
String.raw`www.filius.de A -2147483648 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)',
|
reason: "a TTL is read by Java's Integer.parseInt, in any script's digits",
|
||||||
lines: [
|
lines: [String.raw`www.filius.de A \u0661\u0660 1.2.3.4\u000A`],
|
||||||
String.raw`www.a\u005Cb.de A 3600 1.2.3.4\u000A`,
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
reason: 'Java trims control characters off a line (String.trim)',
|
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(
|
const departures = new Map(
|
||||||
[...SWIFT_READS_JAVA_NEVER_ANSWERS, ...JAVA_ANSWERS_SWIFT_DROPS].flatMap(({ reason, lines }) =>
|
[...SWIFT_READS_JAVA_NEVER_ANSWERS, ...JAVA_ANSWERS_SWIFT_DROPS].flatMap(({ reason, lines }) =>
|
||||||
lines.map((line) => [line, reason] as const),
|
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', () => {
|
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);
|
const lines = readFileSync(join(here, 'dns-hosts.cases.txt'), 'ascii').split('\n').slice(0, -1);
|
||||||
expect(javaMeasured.map(([line]) => line)).toEqual(lines);
|
expect(javaMeasured.map(([line]) => line)).toEqual(lines);
|
||||||
expect(swiftMeasured.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) {
|
for (const [line, expected] of javaMeasured) {
|
||||||
it(JSON.stringify(line), () => {
|
const bug = javaBugs.get(line);
|
||||||
const records = readJavaDnsHosts(unescape(line));
|
it(bug === undefined ? JSON.stringify(line) : `${JSON.stringify(line)} (Java errs: ${bug})`, () => {
|
||||||
expect(records.map((record) => [record.domainName, record.type, record.ttl, record.rdata])).toEqual(expected);
|
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', () => {
|
describe('the records Java answers', () => {
|
||||||
it('leave out records Java never answers: an empty name, or a type other than A, MX, NS', () => {
|
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', () => {
|
it('take names in lower case without the final dot, A targets as FILIUS normalises addresses', () => {
|
||||||
|
|||||||
@@ -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
|
// email-server.cases.txt and email-client.cases.txt was read by Java
|
||||||
// (EmailServer.kontenLaden, EmailAnwendung.laden; *.java.json,
|
// (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 { readFileSync } from 'node:fs';
|
||||||
import { dirname, join } from 'node:path';
|
import { dirname, join } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { describe, expect, it } from 'vitest';
|
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';
|
import { javaEmailAccount } from './javaEmailNames.js';
|
||||||
|
|
||||||
const here = dirname(fileURLToPath(import.meta.url));
|
const here = dirname(fileURLToPath(import.meta.url));
|
||||||
@@ -26,22 +27,112 @@ function measured(kind: 'server' | 'client'): [string, unknown][] {
|
|||||||
return cases;
|
return cases;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('readJavaEmailServerFile against Java', () => {
|
/** Server files where Java errs and this reader does not (see readEmailServerFile). */
|
||||||
for (const [line, expected] of measured('server')) {
|
const SERVER_JAVA_BUGS: { reason: string; lines: string[] }[] = [
|
||||||
it(JSON.stringify(line), () => {
|
{
|
||||||
const accounts = readJavaEmailServerFile(unescape(line));
|
reason: "an empty field counts; Java's StringTokenizer skips it and shifts the rest",
|
||||||
expect(Array.isArray(accounts) ? accounts.map(javaEmailAccount) : `!${accounts.exception}`).toEqual(expected);
|
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', () => {
|
describe('readEmailClientFile against Java, but for its errors', () => {
|
||||||
for (const [line, expected] of measured('client')) {
|
compareWithJava('client', byLine(CLIENT_JAVA_BUGS), (content) => {
|
||||||
it(JSON.stringify(line), () => {
|
const account = readEmailClientFile(content, () => {});
|
||||||
const account = readJavaEmailClientFile(unescape(line));
|
return account === null ? null : javaEmailAccount(account);
|
||||||
expect(account === null ? null : javaEmailAccount(account)).toEqual(expected);
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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', () => {
|
describe('addressEntry', () => {
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
type FlsNodeClass,
|
type FlsNodeClass,
|
||||||
type FlsTopology,
|
type FlsTopology,
|
||||||
type JavaEmailAccount,
|
type JavaEmailAccount,
|
||||||
type JavaFailure,
|
|
||||||
} from '../src/index.js';
|
} from '../src/index.js';
|
||||||
import { EXPECTED_WARNINGS } from './expectedWarnings.js';
|
import { EXPECTED_WARNINGS } from './expectedWarnings.js';
|
||||||
import { javaEmail, javaEmailAccount } from './javaEmailNames.js';
|
import { javaEmail, javaEmailAccount } from './javaEmailNames.js';
|
||||||
@@ -122,9 +121,8 @@ function javaApplications(applications: FlsApplication[]): unknown[] {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function javaFileAccounts(accounts: JavaEmailAccount[] | JavaFailure | null): unknown {
|
function javaFileAccounts(accounts: JavaEmailAccount[] | null): unknown {
|
||||||
if (accounts === null) return null;
|
return accounts === null ? null : accounts.map(javaEmailAccount);
|
||||||
return Array.isArray(accounts) ? accounts.map(javaEmailAccount) : `!${accounts.exception}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Java's property names, as FlsOracle.java writes them. */
|
/** 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', () => {
|
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, so Java
|
// U+3000 separates, U+00A0 does not; the third entry has no IP. Java
|
||||||
// throws there and the fourth is never read.
|
// 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">
|
const { nodes, warnings } = topologyOf(`<object class="java.util.LinkedList">${nodeItem(`<object class="filius.hardware.knoten.Rechner">
|
||||||
<void property="systemSoftware"><void property="DHCPServer">
|
<void property="systemSoftware"><void property="DHCPServer">
|
||||||
<void property="staticAssignedAddresses"><object class="java.util.ArrayList">
|
<void property="staticAssignedAddresses"><object class="java.util.ArrayList">
|
||||||
@@ -393,10 +391,11 @@ describe('readTopology rules', () => {
|
|||||||
staticAssignedAddresses: [
|
staticAssignedAddresses: [
|
||||||
{ mac: '0A:00:00:00:00:01', ip: '10.0.0.1' },
|
{ 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:02', ip: '10.0.0.2\u00A0x' },
|
||||||
|
{ mac: '0A:00:00:00:00:04', ip: '10.0.0.4' },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
expect(warnings).toEqual([
|
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">
|
const { nodes } = topologyOf(`<object class="java.util.LinkedList">${nodeItem(`<object class="filius.hardware.knoten.Rechner">
|
||||||
<void property="systemSoftware"><void property="installierteAnwendungen">
|
<void property="systemSoftware"><void property="installierteAnwendungen">
|
||||||
${put('filius.software.email.EmailServer', `
|
${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="listeBenutzerkonten"><void method="add"><object class="filius.software.email.EmailKonto">
|
||||||
<void property="benutzername"><string>bob</string></void>
|
<void property="benutzername"><string>bob</string></void>
|
||||||
<void property="nachname"><null/></void>
|
<void property="nachname"><null/></void>
|
||||||
@@ -473,7 +472,7 @@ describe('readTopology rules', () => {
|
|||||||
</void></void></object>`)}</object>`);
|
</void></void></object>`)}</object>`);
|
||||||
const server = nodes[0]!.system.applications![0]!;
|
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).toMatchObject({ isActive: false, mailDomain: 'filius.de' });
|
||||||
expect(server.beanAccounts![0]).toMatchObject({ username: 'bob', lastName: '', firstName: '' });
|
expect(server.beanAccounts![0]).toMatchObject({ username: 'bob', lastName: '', firstName: '' });
|
||||||
expect(server.beanAccounts![0]!.messages[0]!.sender).toEqual({ name: 'Alice', mailAddress: 'alice@senior.de' });
|
expect(server.beanAccounts![0]!.messages[0]!.sender).toEqual({ name: 'Alice', mailAddress: 'alice@senior.de' });
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# Java-Orakel
|
# 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
|
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
|
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
|
Dateiformat** — der Swift-Leser verliert in 10 von 23 Dateien Geräte (Plan
|
||||||
|
|||||||
Reference in New Issue
Block a user