feat(fls): file systems of PCs and notebooks as Java loads them

readSystemSoftware reads a host's file system: Java's tree of
DefaultMutableTreeNodes, directories named by their user object, files
as Datei (by setters or the three-argument constructor), rooted at
arbeitsVerzeichnis (up to 1.10) or root. All 23 corpus files match Java,
every file compared by name, type and the SHA-256 of its content.

Routers and home routers have files in Java too, but their constructor
makes them from templates (the start page of the firewall's web
interface); they are not in the file and not claimed here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-29 21:14:01 +00:00
co-authored by Claude Opus 5.5
parent cf47cf8f9d
commit 51a83ea68d
6 changed files with 2936 additions and 5 deletions
+64 -2
View File
@@ -11,8 +11,14 @@
// table (port forwarding) and a modem's connection settings.
// Java counterparts as well: filius.software.dhcp.DHCPServer, ModemFirmware.
//
// Checked against Java (corpus.java.json, `system`). File systems and
// applications are later layers.
// And the file system of a PC or notebook: Java's tree of
// DefaultMutableTreeNodes, a directory's user object its name, a file's a
// filius.software.system.Datei. Routers and home routers have files too, but
// Java's constructor makes them from its own templates (the start page of
// the firewall's web interface); they are not in the file.
//
// Checked against Java (corpus.java.json, `system`). Applications are a
// later layer.
//
// A router's firewall is one object its constructor makes and hangs in two
// places: the installed applications and the configuration plug-ins of its
@@ -67,6 +73,12 @@ export interface FlsModem {
ipAddress: string | null;
}
export type FlsFileTree =
| { kind: 'directory'; name: string | null; children: FlsFileTree[] }
| { kind: 'file'; name: string | null; type: string | null; content: string | null; children: FlsFileTree[] }
/** A node whose user object is neither a name nor a file. */
| { kind: 'other'; className: string; children: FlsFileTree[] };
export interface FlsSystemSoftware {
/**
* Switch: its SSID, undefined when the file has none (Java rolls one).
@@ -89,6 +101,8 @@ export interface FlsSystemSoftware {
staticNAT?: (string | null)[][];
/** Modem. */
modem?: FlsModem;
/** Rechner, Notebook: the root directory (named "root" by Java's constructor). */
fileSystem?: FlsFileTree;
}
type Warn = (message: string) => void;
@@ -110,6 +124,7 @@ export function readSystemSoftware(device: JavaObject, javaClass: FlsNodeClass,
case 'Rechner':
case 'Notebook':
result.ssid = optionalString(value('ssid'), 'ssid', warn) ?? null;
result.fileSystem = fileSystem(value('dateisystem'), warn);
break;
case 'Vermittlungsrechner':
result.ripEnabled = boolean(value('ripEnabled'), false, 'ripEnabled', warn);
@@ -150,6 +165,53 @@ function modemAddress(value: string | null | undefined): string | null {
return value === undefined ? 'localhost' : value;
}
/**
* Dateisystem: its root is `arbeitsVerzeichnis` in files up to 1.10 and
* `root` after. The constructor's root is a node named "root"; a node the
* file makes with `new DefaultMutableTreeNode()` has no user object.
*/
function fileSystem(system: JavaValue | undefined, warn: Warn): FlsFileTree {
const at = (property: string) => (isJavaObject(system) ? system.properties.get(property) : undefined);
const root = at('root') ?? at('arbeitsVerzeichnis');
if (root === undefined) return { kind: 'directory', name: 'root', children: [] };
return fileTreeNode(root, true, warn);
}
function fileTreeNode(node: JavaValue, constructorRoot: boolean, warn: Warn): FlsFileTree {
if (!isJavaObject(node)) {
warn(`file system: a node is ${describe(node)}, left empty`);
return { kind: 'other', className: 'unknown', children: [] };
}
for (const call of node.calls) {
if (call.method !== 'add') warn(`file system: ${call.method} on a node is not read`);
}
const children = listItems(node).map((child) => fileTreeNode(child, false, warn));
const userObject = node.properties.has('userObject')
? node.properties.get('userObject')!
: node.fromGetter && constructorRoot
? 'root'
: (node.args[0] ?? null);
if (userObject === null || typeof userObject === 'string') return { kind: 'directory', name: userObject, children };
if (isJavaObject(userObject) && userObject.className === 'filius.software.system.Datei') {
// new Datei(name, typ, inhalt), then the setters.
const [name, type, content] = userObject.args.length === 3 ? userObject.args : [];
const text = (property: string, fromConstructor: JavaValue | undefined) => {
const value = userObject.properties.get(property) ?? fromConstructor ?? null;
if (typeof value === 'string' || value === null) return value;
warn(`file system: file ${property} is ${describe(value)}, ignored`);
return null;
};
return {
kind: 'file',
name: text('name', name),
type: text('dateiTyp', type),
content: text('dateiInhalt', content),
children,
};
}
return { kind: 'other', className: isJavaObject(userObject) ? userObject.className : typeof userObject, children };
}
/** A list of String arrays (routes, static NAT); anything but a string is null. */
function rowsOfStrings(list: JavaValue | undefined): (string | null)[][] {
return listItems(list).map((row) => listItems(row).map((cell) => (typeof cell === 'string' ? cell : null)));