feat(core): RIP after Java's code

Where the Swift template departs from Java's filius.software.rip, Java's
code rules: a learned route gets 16 hops at the router's next beacon after
it expired (RIPTable.check), not when it expires; messages count from any
source port; each router draws its pauses from a random of its own (Java:
an unseeded Random per router), which the core seeds from the scenario and
the router's id (ripSeed), so that the Java oracle can give it the same.

A route of hop count 0 to each own network, the first beacon after a
second, then every 25.2 to 35.2 s; a message per interface without the
routes through it; a new route or a longer one from the same router sends
at once; forwarding by the table alone, fewest hops before longest mask.
Messages are read with Java's parser (javaIpCheck, javaParseInt, moved from
fls to core for it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 19:29:48 +00:00
co-authored by Claude Opus 5.5
parent 313786e738
commit a4036f12a3
10 changed files with 522 additions and 45 deletions
+65
View File
@@ -0,0 +1,65 @@
// What Java FILIUS stores when a file sets an interface's IP address or mask:
// NetzwerkInterface.setIp(ip) keeps IP.ipCheck(ip), which is null for text
// that is not an address and the canonical dotted quad otherwise.
// Java counterpart: filius.software.vermittlungsschicht.IP.inetAton/ipCheck.
//
// Measured against Java (test/ip-check.cases.txt, tools/java-oracle), since
// the rules are wider than they look:
// - StringTokenizer(".") skips empty tokens: "10..0.0.1" is 10.0.0.1;
// - Integer.parseInt takes a sign and leading zeros: "+010" is 10, "-0" is 0;
// - and any Unicode decimal digit in the BMP: Arabic-Indic "١٠" is 10 —
// parseInt reads UTF-16 units, so "𝟏" (a surrogate pair) is no digit.
/**
* `Integer.parseInt(text)`: an optional sign, then one or more decimal digits
* of any script; undefined where Java throws NumberFormatException.
*/
export function javaParseInt(text: string): number | undefined {
const characters = text.split('');
let index = 0;
let negative = false;
if (characters[0] === '-' || characters[0] === '+') {
negative = characters[0] === '-';
index = 1;
}
if (index >= characters.length) return undefined;
let value = 0;
for (; index < characters.length; index++) {
const digit = decimalDigitValue(characters[index]!);
if (digit === undefined) return undefined;
value = value * 10 + digit;
// Past the int range Java throws; the octet check would reject it anyway.
if (value > 2 ** 31) return undefined;
}
if (!negative && value > 2 ** 31 - 1) return undefined;
return negative ? -value : value;
}
/**
* `Character.digit(c, 10)` for one UTF-16 unit: Unicode lays out every
* script's digits as a run of ten from zero to nine, so the value is the
* distance from the start of the run, modulo ten (in case two runs touch).
*/
function decimalDigitValue(unit: string): number | undefined {
if (!/^\p{Nd}$/u.test(unit)) return undefined;
let code = unit.charCodeAt(0);
let offset = 0;
while (/^\p{Nd}$/u.test(String.fromCharCode(code - 1))) {
code -= 1;
offset += 1;
}
return offset % 10;
}
/** `IP.ipCheck(text)`: four octets 0…255 as `a.b.c.d`, or null. */
export function javaIpCheck(text: string): string | null {
const tokens = text.split('.').filter((token) => token !== '');
if (tokens.length !== 4) return null;
const octets: number[] = [];
for (const token of tokens) {
const octet = javaParseInt(token);
if (octet === undefined || octet < 0 || octet > 255) return null;
octets.push(octet);
}
return octets.join('.');
}