The Java view now reads what FILIUS 2.14 keeps for its e-mail programs: the server's domain, switch and bean accounts with their mailboxes, and the accounts of /mailserver/konten.txt, which Java reads at every simulation start and which so win over the bean list; the client's three folders and address book, and its account from /konten.txt (no bean holds it). javaEmail.ts ports the bean setters and both file readers (stringZuListeBenutzerkonten, EmailKonto(String)); javaString.ts Java's trim, split and StringTokenizer; javaFileSystem.ts the file lookup, now shared with the DNS server, with the four non-ASCII units Java's equalsIgnoreCase also takes for an ASCII letter (measured over the BMP). Checked against Java on the corpus (the oracle dumps the e-mail state) and on 43 account files read by EmailServer.kontenLaden and EmailAnwendung.laden (EmailFileCases). Found: older files keep recipients as strings in the typed lists; a getter copy of the sender is filled and set back; one short server line makes Java throw for the whole file; a message with an empty text is lost on reading; a broken last client line empties the account. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
188 lines
7.3 KiB
TypeScript
188 lines
7.3 KiB
TypeScript
// 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.
|
|
|
|
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';
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
type Row = [string, string, number, string];
|
|
const javaMeasured = JSON.parse(readFileSync(join(here, 'dns-hosts.java.json'), 'utf8')) as [string, Row[]][];
|
|
const swiftMeasured = JSON.parse(readFileSync(join(here, 'dns-hosts.swift.json'), 'utf8')) as [string, Row[]][];
|
|
|
|
/** The cases file's escapes: a backslash, `u` and four hex digits for one UTF-16 unit. */
|
|
function unescape(line: string): string {
|
|
return line.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex: string) => String.fromCharCode(Number.parseInt(hex, 16)));
|
|
}
|
|
|
|
function modelRows(line: string, warnings: string[] = []): Row[] {
|
|
return dnsRecordsOf(readJavaDnsHosts(unescape(line)), (message) => warnings.push(message)).map((record) => [
|
|
record.name,
|
|
record.type,
|
|
record.ttlSeconds,
|
|
record.target,
|
|
]);
|
|
}
|
|
|
|
/** 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: [
|
|
String.raw`www.filius.de\u0009A\u00093600\u00091.2.3.4\u000A`,
|
|
String.raw`www.filius.de\u00A0A 3600 1.2.3.4\u000A`,
|
|
String.raw`www.filius.de A 3600 1.2.3.4\u00A0\u000A`,
|
|
String.raw`www.filius.de \u0009 A 3600 1.2.3.4\u000A`,
|
|
String.raw`www.filius.de A \u0009 3600 1.2.3.4\u000A`,
|
|
],
|
|
},
|
|
{
|
|
reason: 'Java splits lines at LF only: CR, LS and NEL stay in the line',
|
|
lines: [
|
|
String.raw`www.filius.de A 3600 1.2.3.4\u000Dmail.filius.de A 3600 1.2.3.5\u000D`,
|
|
String.raw`www.filius.de A 3600 1.2.3.4\u2028mail.filius.de A 3600 1.2.3.5\u000A`,
|
|
String.raw`www.filius.de A 3600 1.2.3.4\u0085\u000A`,
|
|
],
|
|
},
|
|
{
|
|
reason: 'Java has no comments, no "IN" and no zone-file order',
|
|
lines: [
|
|
String.raw`www.filius.de A 3600 1.2.3.4#x\u000A`,
|
|
String.raw`www.filius.de A IN 3600 1.2.3.4\u000A`,
|
|
String.raw`www.filius.de 3600 IN A 1.2.3.4\u000A`,
|
|
],
|
|
},
|
|
{
|
|
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: 'an MX or NS target must be a name Java accepts: no preference, no address, one final dot',
|
|
lines: [
|
|
String.raw`filius.de MX 3600 10 mail.filius.de\u000A`,
|
|
String.raw`filius.de MX 3600 1.2.3.4\u000A`,
|
|
String.raw`filius.de NS 3600 dns.filius.de..\u000A`,
|
|
],
|
|
},
|
|
];
|
|
|
|
/** 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',
|
|
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: 'Java trims control characters off a line (String.trim)',
|
|
lines: [
|
|
String.raw`www.filius.de A 3600 1.2.3.4\u0001\u000A`,
|
|
String.raw`\u001Fwww.filius.de A 3600 1.2.3.4\u000A`,
|
|
],
|
|
},
|
|
{
|
|
reason: 'an NS record may name the root',
|
|
lines: [
|
|
String.raw`de. NS 3600 .\u000A`,
|
|
],
|
|
},
|
|
{
|
|
reason: 'duplicates stay, as in Java\'s list',
|
|
lines: [
|
|
String.raw`www.filius.de A 3600 1.2.3.4\u000Awww.filius.de A 3600 1.2.3.4\u000A`,
|
|
String.raw`www.filius.de A 3600 1.2.3.4\u000Awww.filius.de A 7200 1.2.3.4\u000A`,
|
|
String.raw`www.filius.de A 3600 1.2.3.4\u000AWWW.filius.de. A 3600 1.2.3.4\u000A`,
|
|
],
|
|
},
|
|
];
|
|
|
|
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', () => {
|
|
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);
|
|
});
|
|
|
|
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);
|
|
});
|
|
}
|
|
});
|
|
|
|
describe('the records the model keeps, against Swift', () => {
|
|
it('names only cases of the cases file as departures', () => {
|
|
const lines = new Set(swiftMeasured.map(([line]) => line));
|
|
expect([...departures.keys()].filter((line) => !lines.has(line))).toEqual([]);
|
|
});
|
|
|
|
for (const [line, swift] of swiftMeasured) {
|
|
const reason = departures.get(line);
|
|
it(reason === undefined ? JSON.stringify(line) : `${JSON.stringify(line)} (departs: ${reason})`, () => {
|
|
if (reason === undefined) expect(modelRows(line)).toEqual(swift);
|
|
else expect(modelRows(line)).not.toEqual(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([]);
|
|
});
|
|
|
|
it('take names in lower case without the final dot, A targets as FILIUS normalises addresses', () => {
|
|
expect(modelRows(String.raw`WWW.Filius.DE. A 60 010.000.0.1\u000Afilius.de MX 3600 Mail.Filius.DE\u000A. NS 3600 dns-root`)).toEqual([
|
|
['www.filius.de', 'A', 60, '10.0.0.1'],
|
|
['filius.de', 'MX', 3600, 'mail.filius.de.'],
|
|
['.', 'NS', 3600, 'dns-root.'],
|
|
]);
|
|
});
|
|
|
|
it('warn about an A record Java answers with an address FILIUS cannot use', () => {
|
|
const warnings: string[] = [];
|
|
expect(modelRows(String.raw`www.filius.de A 3600 .1.2.3`, warnings)).toEqual([]);
|
|
expect(warnings).toEqual(["DNS record 'www.filius.de A .1.2.3' has no usable IPv4 address, left out (it stays in /dns/hosts)"]);
|
|
});
|
|
});
|