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>
147 lines
7.0 KiB
TypeScript
147 lines
7.0 KiB
TypeScript
// 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 — 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, readEmailClientFile, readEmailServerFile } from '../src/javaEmail.js';
|
|
import { javaEmailAccount } from './javaEmailNames.js';
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
|
|
/** The cases file's escapes: a backslash, `u` and four hex digits for one UTF-16 unit. */
|
|
function unescape(line: string): string {
|
|
return line.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex: string) => String.fromCharCode(Number.parseInt(hex, 16)));
|
|
}
|
|
|
|
function measured(kind: 'server' | 'client'): [string, unknown][] {
|
|
const cases = JSON.parse(readFileSync(join(here, `email-${kind}.java.json`), 'utf8')) as [string, unknown][];
|
|
const lines = readFileSync(join(here, `email-${kind}.cases.txt`), 'ascii').split('\n').slice(0, -1);
|
|
it('covers every line of the cases file', () => {
|
|
expect(cases.map(([line]) => line)).toEqual(lines);
|
|
});
|
|
return cases;
|
|
}
|
|
|
|
/** 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('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', () => {
|
|
it('reads a name and an address, an address alone, or a name alone', () => {
|
|
expect(addressEntry(' Alice <alice@senior.de> ')).toEqual({ name: 'Alice', mailAddress: 'alice@senior.de' });
|
|
expect(addressEntry('<alice@senior.de>')).toEqual({ name: null, mailAddress: 'alice@senior.de' });
|
|
expect(addressEntry('alice@senior.de')).toEqual({ name: null, mailAddress: 'alice@senior.de' });
|
|
expect(addressEntry('Alice <alice@senior.de')).toEqual({ name: 'Alice <alice@senior.de', mailAddress: null });
|
|
expect(addressEntry('Alice')).toEqual({ name: 'Alice', mailAddress: null });
|
|
});
|
|
});
|