fix(fls): Gnutella's peer limit is random in Java, not a default

PeerToPeerAnwendung's constructor rolls its peer limit (3, 4 or 5). The
getter defaults measured it once each run — 4, then 5 — and c48d15d
committed 5 as a default. GetterDefaults now constructs every class 64
times and writes a value that varies as {"random": [...]}; the decoder
answers a reference to it as unresolved, and the topology layer reports
a peer limit the file does not set as null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 06:12:14 +00:00
co-authored by Claude Opus 5.5
parent c48d15def4
commit 8080e7dcb2
6 changed files with 65 additions and 17 deletions
+7 -1
View File
@@ -11,7 +11,13 @@
"JSidebarButton#iconTextGap": 4,
"JSidebarButton#verticalTextPosition": 3,
"Notebook#maxLayerOfOperation": 4,
"PeerToPeerAnwendung#maxTeilnehmerZahl": 5,
"PeerToPeerAnwendung#maxTeilnehmerZahl": {
"random": [
"3",
"4",
"5"
]
},
"RIPServer#port": 520,
"Rechner#maxLayerOfOperation": 4,
"ServerBaustein#aktiv": false,
+24 -6
View File
@@ -217,15 +217,30 @@ export function decodeXmlBytes(bytes: Uint8Array): string {
// ── Decoder ────────────────────────────────────────────────────────────────
const PRIMITIVE_ID_PREFIXES = new Set(['Integer', 'Boolean', 'Long', 'Short', 'Byte', 'Float', 'Double', 'Character']);
const DEFAULTS = javaDefaults as Readonly<Record<string, string | number | boolean | null>>;
type MeasuredValue = string | number | boolean | null;
/** A value a constructor rolls at random: measured as the set of values seen, never a default. */
type RandomValue = { random: string[] };
const MEASURED = javaDefaults as Readonly<Record<string, MeasuredValue | RandomValue>>;
/** The measured constant under `key`; undefined if nothing was measured or Java rolls it. */
function measuredConstant(key: string): MeasuredValue | undefined {
if (!Object.hasOwn(MEASURED, key)) return undefined;
const value = MEASURED[key]!;
return typeof value === 'object' && value !== null ? undefined : value;
}
function isRandomInJava(key: string): boolean {
const value = MEASURED[key];
return typeof value === 'object' && value !== null;
}
/**
* What a getter returns on a freshly constructed object of `simpleClass`, as
* measured (javaDefaults.json) — undefined where nothing was measured.
* measured (javaDefaults.json) — undefined where nothing was measured or
* where Java's constructor rolls the value.
*/
export function javaDefault(simpleClass: string, property: string): string | number | boolean | null | undefined {
const key = `${simpleClass}#${property}`;
return Object.hasOwn(DEFAULTS, key) ? DEFAULTS[key] : undefined;
return measuredConstant(`${simpleClass}#${property}`);
}
/** `getFoo`/`isFoo` → `foo`, as java.beans.Introspector derives it. */
@@ -356,7 +371,8 @@ class Decoder {
// A static constant: XMLEncoder writes a shared Integer 0 as
// SwingConstants.CENTER when that was the first 0 it met.
const constantKey = `${simpleClassName(className)}.${attributes.field}`;
if (Object.hasOwn(DEFAULTS, constantKey)) return this.primitive(element, DEFAULTS[constantKey]!);
const constant = measuredConstant(constantKey);
if (constant !== undefined) return this.primitive(element, constant);
}
const args = element.children.filter((child) => child.name !== 'void');
const object = newObject({
@@ -457,7 +473,9 @@ class Decoder {
}
if (target.properties.has(property)) return target.properties.get(property)!;
const defaultKey = `${simpleClassName(target.className)}#${property}`;
if (Object.hasOwn(DEFAULTS, defaultKey)) return DEFAULTS[defaultKey]!;
const measured = measuredConstant(defaultKey);
if (measured !== undefined) return measured;
if (isRandomInJava(defaultKey)) return { kind: 'unresolved', reason: `${defaultKey} is random in Java` };
const prefix = id?.replace(/\d+$/, '');
if (prefix !== undefined && PRIMITIVE_ID_PREFIXES.has(prefix)) {
return { kind: 'unresolved', reason: `no measured default for ${defaultKey}` };
+7 -3
View File
@@ -129,8 +129,11 @@ export interface FlsApplication {
recursiveResolutionEnabled?: boolean;
/** WebServer: serve by host name (Java's useVHost). */
useVirtualHosts?: boolean;
/** PeerToPeerAnwendung: how many peers it keeps (Java's maxTeilnehmerZahl). */
maxPeers?: number;
/**
* PeerToPeerAnwendung: how many peers it keeps (Java's maxTeilnehmerZahl);
* null when the file has none — Java's constructor rolls 3, 4 or 5.
*/
maxPeers?: number | null;
/** Firewall: the personal firewall. */
firewall?: FlsFirewall;
/** The application's JavaBean, for the e-mail layer. */
@@ -269,7 +272,8 @@ function applications(map: JavaValue | undefined, warn: Warn): FlsApplication[]
entry.useVirtualHosts = boolean(at('useVHost'), measured('useVHost') === true, 'WebServer useVHost', warn);
break;
case 'filius.software.dateiaustausch.PeerToPeerAnwendung':
entry.maxPeers = integer(at('maxTeilnehmerZahl'), Number(measured('maxTeilnehmerZahl')), 'Gnutella peers', warn);
entry.maxPeers =
at('maxTeilnehmerZahl') === undefined ? null : integer(at('maxTeilnehmerZahl'), 0, 'Gnutella peers', warn);
break;
case 'filius.software.firewall.Firewall':
entry.firewall = firewallOf([application], true, warn);