feat(fls): system software settings as Java loads them

readSystemSoftware adds to each node what the topology needs from its
system software: a switch's SSID and learning time, a host's wireless
SSID, a router's RIP switch, the manual routes and the firewall of a
router or home router. All 23 corpus files match Java.

- A router's firewall is one object its constructor hangs in two places;
  the corpus writes it under the web server's "konfig" plug-in. The
  places are merged.
- The decoder keeps entries a constructor made in a map, reached by
  get(key) (installed applications), like the elements of a list.
- The Java oracle loads every file twice and writes values that differ
  as null: what FILIUS rolls (MACs, a switch's SSID) without text search.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-29 20:56:10 +00:00
co-authored by Claude Opus 5.5
parent da2ec1dae2
commit 9a389810f9
10 changed files with 1851 additions and 228 deletions
+32 -4
View File
@@ -53,6 +53,11 @@ export interface JavaObject {
* result. `add`/`set` go to `calls`, in order.
*/
readonly presetElements: Map<number, JavaValue>;
/**
* The same for a map: entries its constructor made (a router's installed
* applications), reached by `<void method="get"><string>key</string>`.
*/
readonly presetEntries: Map<string, JavaValue>;
/** True when the object was not in the file but returned by a getter. */
readonly fromGetter: boolean;
}
@@ -139,12 +144,21 @@ function replayList(value: JavaObject, constructed: number): (JavaValue | undefi
return items;
}
/** The entries of a map: `put` calls in order. */
/**
* The entries of a map: the ones its constructor made, then `put` calls in
* order — a later `put` of the same key replaces the value in place.
*/
export function mapEntries(value: JavaValue | undefined): [JavaValue, JavaValue][] {
if (!isJavaObject(value)) return [];
return value.calls
.filter((call) => call.method === 'put')
.map((call) => [call.args[0] ?? null, call.args[1] ?? null] as [JavaValue, JavaValue]);
const entries: [JavaValue, JavaValue][] = [...value.presetEntries];
for (const call of value.calls) {
if (call.method !== 'put') continue;
const key = call.args[0] ?? null;
const existing = entries.find(([entryKey]) => entryKey === key);
if (existing === undefined) entries.push([key, call.args[1] ?? null]);
else existing[1] = call.args[1] ?? null;
}
return entries;
}
// ── XML tree ───────────────────────────────────────────────────────────────
@@ -226,6 +240,7 @@ function newObject(fields: Partial<JavaObject> & { className: string }): JavaObj
properties: new Map(),
calls: [],
presetElements: new Map(),
presetEntries: new Map(),
fromGetter: fields.fromGetter ?? false,
};
}
@@ -412,6 +427,7 @@ class Decoder {
if (isJavaObject(target)) {
target.calls.push({ method, args });
if (method === 'get' && args.length === 1 && typeof args[0] === 'number') return this.getIndex(target, args[0], id);
if (method === 'get' && args.length === 1 && typeof args[0] === 'string') return this.getEntry(target, args[0], id);
}
return null;
}
@@ -463,6 +479,18 @@ class Decoder {
return placeholder;
}
/**
* `map.get(key)`: the value put before, else an entry the map held before
* the file touched it — a placeholder that later reads return.
*/
private getEntry(target: JavaObject, key: string, id: string | undefined): JavaValue {
const put = mapEntries(target).find(([entryKey]) => entryKey === key);
if (put !== undefined) return put[1];
const placeholder = newObject({ className: id?.replace(/\d+$/, '') ?? 'unknown', id, fromGetter: true });
target.presetEntries.set(key, placeholder);
return placeholder;
}
private setIndex(target: JavaValue | undefined, index: number, value: JavaValue): JavaValue {
if (typeof target === 'object' && target !== null && (target as JavaArray).kind === 'array') {
(target as JavaArray).items[index] = value;