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:
@@ -11,7 +11,13 @@
|
|||||||
"JSidebarButton#iconTextGap": 4,
|
"JSidebarButton#iconTextGap": 4,
|
||||||
"JSidebarButton#verticalTextPosition": 3,
|
"JSidebarButton#verticalTextPosition": 3,
|
||||||
"Notebook#maxLayerOfOperation": 4,
|
"Notebook#maxLayerOfOperation": 4,
|
||||||
"PeerToPeerAnwendung#maxTeilnehmerZahl": 5,
|
"PeerToPeerAnwendung#maxTeilnehmerZahl": {
|
||||||
|
"random": [
|
||||||
|
"3",
|
||||||
|
"4",
|
||||||
|
"5"
|
||||||
|
]
|
||||||
|
},
|
||||||
"RIPServer#port": 520,
|
"RIPServer#port": 520,
|
||||||
"Rechner#maxLayerOfOperation": 4,
|
"Rechner#maxLayerOfOperation": 4,
|
||||||
"ServerBaustein#aktiv": false,
|
"ServerBaustein#aktiv": false,
|
||||||
|
|||||||
@@ -217,15 +217,30 @@ export function decodeXmlBytes(bytes: Uint8Array): string {
|
|||||||
// ── Decoder ────────────────────────────────────────────────────────────────
|
// ── Decoder ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const PRIMITIVE_ID_PREFIXES = new Set(['Integer', 'Boolean', 'Long', 'Short', 'Byte', 'Float', 'Double', 'Character']);
|
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
|
* 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 {
|
export function javaDefault(simpleClass: string, property: string): string | number | boolean | null | undefined {
|
||||||
const key = `${simpleClass}#${property}`;
|
return measuredConstant(`${simpleClass}#${property}`);
|
||||||
return Object.hasOwn(DEFAULTS, key) ? DEFAULTS[key] : undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** `getFoo`/`isFoo` → `foo`, as java.beans.Introspector derives it. */
|
/** `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
|
// A static constant: XMLEncoder writes a shared Integer 0 as
|
||||||
// SwingConstants.CENTER when that was the first 0 it met.
|
// SwingConstants.CENTER when that was the first 0 it met.
|
||||||
const constantKey = `${simpleClassName(className)}.${attributes.field}`;
|
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 args = element.children.filter((child) => child.name !== 'void');
|
||||||
const object = newObject({
|
const object = newObject({
|
||||||
@@ -457,7 +473,9 @@ class Decoder {
|
|||||||
}
|
}
|
||||||
if (target.properties.has(property)) return target.properties.get(property)!;
|
if (target.properties.has(property)) return target.properties.get(property)!;
|
||||||
const defaultKey = `${simpleClassName(target.className)}#${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+$/, '');
|
const prefix = id?.replace(/\d+$/, '');
|
||||||
if (prefix !== undefined && PRIMITIVE_ID_PREFIXES.has(prefix)) {
|
if (prefix !== undefined && PRIMITIVE_ID_PREFIXES.has(prefix)) {
|
||||||
return { kind: 'unresolved', reason: `no measured default for ${defaultKey}` };
|
return { kind: 'unresolved', reason: `no measured default for ${defaultKey}` };
|
||||||
|
|||||||
@@ -129,8 +129,11 @@ export interface FlsApplication {
|
|||||||
recursiveResolutionEnabled?: boolean;
|
recursiveResolutionEnabled?: boolean;
|
||||||
/** WebServer: serve by host name (Java's useVHost). */
|
/** WebServer: serve by host name (Java's useVHost). */
|
||||||
useVirtualHosts?: boolean;
|
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: the personal firewall. */
|
||||||
firewall?: FlsFirewall;
|
firewall?: FlsFirewall;
|
||||||
/** The application's JavaBean, for the e-mail layer. */
|
/** 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);
|
entry.useVirtualHosts = boolean(at('useVHost'), measured('useVHost') === true, 'WebServer useVHost', warn);
|
||||||
break;
|
break;
|
||||||
case 'filius.software.dateiaustausch.PeerToPeerAnwendung':
|
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;
|
break;
|
||||||
case 'filius.software.firewall.Firewall':
|
case 'filius.software.firewall.Firewall':
|
||||||
entry.firewall = firewallOf([application], true, warn);
|
entry.firewall = firewallOf([application], true, warn);
|
||||||
|
|||||||
@@ -240,6 +240,17 @@ describe('XMLDecoder rules', () => {
|
|||||||
expect(isUnresolved(listItems(holder)[0])).toBe(true);
|
expect(isUnresolved(listItems(holder)[0])).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not take a value Java rolls in its constructor as a default', () => {
|
||||||
|
// Gnutella's peer limit is 3, 4 or 5 at random (measured by constructing it 64 times).
|
||||||
|
const [, holder] = decodeJavaBeans(
|
||||||
|
doc(`<object class="filius.software.dateiaustausch.PeerToPeerAnwendung"><void id="Integer0" property="maxTeilnehmerZahl"/></object>
|
||||||
|
<object class="java.util.ArrayList"><void method="add"><object idref="Integer0"/></void></object>`),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [item] = listItems(holder);
|
||||||
|
expect(item).toEqual({ kind: 'unresolved', reason: 'PeerToPeerAnwendung#maxTeilnehmerZahl is random in Java' });
|
||||||
|
});
|
||||||
|
|
||||||
it('reports an idref to an id that was never defined', () => {
|
it('reports an idref to an id that was never defined', () => {
|
||||||
const [value] = decodeJavaBeans(doc('<object idref="Missing0"/>'));
|
const [value] = decodeJavaBeans(doc('<object idref="Missing0"/>'));
|
||||||
|
|
||||||
|
|||||||
@@ -416,7 +416,8 @@ describe('readTopology rules', () => {
|
|||||||
|
|
||||||
expect(byClass.get('filius.software.www.WebServer')).toMatchObject({ isActive: false, port: 80, useVirtualHosts: false });
|
expect(byClass.get('filius.software.www.WebServer')).toMatchObject({ isActive: false, port: 80, useVirtualHosts: false });
|
||||||
expect(byClass.get('filius.software.clientserver.ClientBaustein')).toMatchObject({ targetAddress: null, targetPort: 55_555 });
|
expect(byClass.get('filius.software.clientserver.ClientBaustein')).toMatchObject({ targetAddress: null, targetPort: 55_555 });
|
||||||
expect(byClass.get('filius.software.dateiaustausch.PeerToPeerAnwendung')).toMatchObject({ maxPeers: 4 });
|
// Java rolls it: no default.
|
||||||
|
expect(byClass.get('filius.software.dateiaustausch.PeerToPeerAnwendung')).toMatchObject({ maxPeers: null });
|
||||||
expect(byClass.get('filius.software.firewall.Firewall')?.firewall).toMatchObject({ activated: true, defaultPolicy: 0 });
|
expect(byClass.get('filius.software.firewall.Firewall')?.firewall).toMatchObject({ activated: true, defaultPolicy: 0 });
|
||||||
expect(byClass.get('filius.software.dns.DNSServer')).toMatchObject({ isActive: true, port: 5353, recursiveResolutionEnabled: false });
|
expect(byClass.get('filius.software.dns.DNSServer')).toMatchObject({ isActive: true, port: 5353, recursiveResolutionEnabled: false });
|
||||||
expect(Object.keys(byClass.get('filius.software.lokal.Terminal')!).sort()).toEqual(['javaClass', 'key', 'source']);
|
expect(Object.keys(byClass.get('filius.software.lokal.Terminal')!).sort()).toEqual(['javaClass', 'key', 'source']);
|
||||||
|
|||||||
@@ -34,12 +34,20 @@ public class GetterDefaults {
|
|||||||
String property = key.substring(key.indexOf('#') + 1);
|
String property = key.substring(key.indexOf('#') + 1);
|
||||||
String simpleName = className.substring(className.lastIndexOf('.') + 1);
|
String simpleName = className.substring(className.lastIndexOf('.') + 1);
|
||||||
try {
|
try {
|
||||||
Object value = className.equals("java.lang.ThreadGroup")
|
// Constructed many times: a constructor may roll its value (Gnutella's
|
||||||
// A Thread's uncaughtExceptionHandler is its group; XMLDecoder runs
|
// peer limit is 3, 4 or 5 at random). Such a value is no default and is
|
||||||
// in the main thread, so that is the group asked.
|
// written as {"random": [the values seen]}.
|
||||||
? read(Thread.currentThread().getThreadGroup(), property)
|
java.util.TreeSet<String> seen = new java.util.TreeSet<>();
|
||||||
: read(Class.forName(className).getDeclaredConstructor().newInstance(), property);
|
Object value = null;
|
||||||
result.put(simpleName + "#" + property, value);
|
for (int attempt = 0; attempt < 64; attempt++) {
|
||||||
|
value = className.equals("java.lang.ThreadGroup")
|
||||||
|
// A Thread's uncaughtExceptionHandler is its group; XMLDecoder runs
|
||||||
|
// in the main thread, so that is the group asked.
|
||||||
|
? read(Thread.currentThread().getThreadGroup(), property)
|
||||||
|
: read(Class.forName(className).getDeclaredConstructor().newInstance(), property);
|
||||||
|
seen.add(String.valueOf(value));
|
||||||
|
}
|
||||||
|
result.put(simpleName + "#" + property, seen.size() == 1 ? value : Map.of("random", new java.util.ArrayList<>(seen)));
|
||||||
} catch (Throwable error) {
|
} catch (Throwable error) {
|
||||||
Throwable cause = error.getCause() != null ? error.getCause() : error;
|
Throwable cause = error.getCause() != null ? error.getCause() : error;
|
||||||
result.put(simpleName + "#" + property, Map.of("error", cause.toString()));
|
result.put(simpleName + "#" + property, Map.of("error", cause.toString()));
|
||||||
|
|||||||
Reference in New Issue
Block a user