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:
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user