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#verticalTextPosition": 3,
|
||||
"Notebook#maxLayerOfOperation": 4,
|
||||
"PeerToPeerAnwendung#maxTeilnehmerZahl": 5,
|
||||
"PeerToPeerAnwendung#maxTeilnehmerZahl": {
|
||||
"random": [
|
||||
"3",
|
||||
"4",
|
||||
"5"
|
||||
]
|
||||
},
|
||||
"RIPServer#port": 520,
|
||||
"Rechner#maxLayerOfOperation": 4,
|
||||
"ServerBaustein#aktiv": false,
|
||||
|
||||
@@ -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}` };
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -240,6 +240,17 @@ describe('XMLDecoder rules', () => {
|
||||
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', () => {
|
||||
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.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.dns.DNSServer')).toMatchObject({ isActive: true, port: 5353, recursiveResolutionEnabled: false });
|
||||
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 simpleName = className.substring(className.lastIndexOf('.') + 1);
|
||||
try {
|
||||
Object value = className.equals("java.lang.ThreadGroup")
|
||||
// Constructed many times: a constructor may roll its value (Gnutella's
|
||||
// peer limit is 3, 4 or 5 at random). Such a value is no default and is
|
||||
// written as {"random": [the values seen]}.
|
||||
java.util.TreeSet<String> seen = new java.util.TreeSet<>();
|
||||
Object value = null;
|
||||
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);
|
||||
result.put(simpleName + "#" + property, value);
|
||||
seen.add(String.valueOf(value));
|
||||
}
|
||||
result.put(simpleName + "#" + property, seen.size() == 1 ? value : Map.of("random", new java.util.ArrayList<>(seen)));
|
||||
} catch (Throwable error) {
|
||||
Throwable cause = error.getCause() != null ? error.getCause() : error;
|
||||
result.put(simpleName + "#" + property, Map.of("error", cause.toString()));
|
||||
|
||||
Reference in New Issue
Block a user