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
+73 -1
View File
@@ -3,6 +3,7 @@
// position and interfaces, each cable with both ends, each documentation
// item — field by field. Then the rules the corpus does not exercise.
import { createHash } from 'node:crypto';
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { dirname, join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -12,6 +13,7 @@ import {
readFlsArchive,
readTopology,
type FlsDhcpServer,
type FlsFileTree,
type FlsModem,
type FlsNodeClass,
type FlsTopology,
@@ -42,6 +44,25 @@ const TYPE_LABELS: Record<FlsNodeClass, string> = {
Modem: 'Modem',
};
/** FlsOracle.fileTree's shape: contents as length and SHA-256 of their UTF-8 bytes. */
function javaFileTree(tree: FlsFileTree): unknown {
const children = tree.children.map(javaFileTree);
switch (tree.kind) {
case 'directory':
return { directory: tree.name, children };
case 'file':
return {
file: tree.name,
type: tree.type,
length: tree.content?.length ?? null,
sha256: tree.content === null ? null : createHash('sha256').update(tree.content, 'utf8').digest('hex'),
children,
};
case 'other':
return { userObject: tree.className, children };
}
}
/** Java's property names, as FlsOracle.java writes them. */
function javaDhcpServer(server: FlsDhcpServer) {
return {
@@ -84,6 +105,7 @@ function asJavaView(topology: FlsTopology, javaBounds: (number[] | null)[]) {
dhcpServer: javaDhcpServer(node.system.dhcpServer),
}),
...(node.system.modem === undefined ? {} : { modem: javaModem(node.system.modem) }),
...(node.system.fileSystem === undefined ? {} : { fileSystem: javaFileTree(node.system.fileSystem) }),
...(node.system.firewall === undefined
? {}
: {
@@ -116,6 +138,23 @@ function asJavaView(topology: FlsTopology, javaBounds: (number[] | null)[]) {
};
}
/**
* Java's view without the file systems of routers and home routers: their
* files come from Java's constructor (templates of the firewall's web
* interface), not from the .fls file, and the reader does not claim them.
*/
function withoutTemplateFiles(view: unknown): unknown {
const { nodes, ...rest } = view as { nodes: { class: string; system: Record<string, unknown> }[] };
return {
...rest,
nodes: nodes.map((node) => {
if (node.class !== 'Vermittlungsrechner' && node.class !== 'Gateway') return node;
const { fileSystem: _templateFiles, ...system } = node.system;
return { ...node, system };
}),
};
}
describe('corpus: the topology as Java loads it', () => {
for (const file of flsFiles(corpusRoot)) {
const name = relative(corpusRoot, file);
@@ -124,7 +163,7 @@ describe('corpus: the topology as Java loads it', () => {
const topology = readTopology(values);
const expected = javaView[name] as { nodes: { bounds: number[] | null }[] };
expect(asJavaView(topology, expected.nodes.map((node) => node.bounds))).toEqual(expected);
expect(asJavaView(topology, expected.nodes.map((node) => node.bounds))).toEqual(withoutTemplateFiles(expected));
expect(topology.warnings).toEqual([]);
});
}
@@ -296,6 +335,39 @@ describe('readTopology rules', () => {
]);
});
it('reads a file system from `root` or `arbeitsVerzeichnis`, with files by setters or constructor', () => {
const { nodes, warnings } = topologyOf(`<object class="java.util.LinkedList">
${nodeItem('<object class="filius.hardware.knoten.Notebook"/>')}
${nodeItem(`<object class="filius.hardware.knoten.Rechner"><void property="systemSoftware"><void property="dateisystem">
<void property="root"><object class="javax.swing.tree.DefaultMutableTreeNode"><string>/</string>
<void method="add"><object class="javax.swing.tree.DefaultMutableTreeNode">
<void method="add"><object class="javax.swing.tree.DefaultMutableTreeNode">
<object class="filius.software.system.Datei"><string>a.txt</string><string>text</string><string>hallo</string>
<void property="dateiTyp"><string>txt</string></void>
</object>
</object></void>
</object></void>
<void method="insert"><object class="javax.swing.tree.DefaultMutableTreeNode"/><int>0</int></void>
</object></void>
</void></void></object>`)}</object>`);
expect(nodes.map((node) => node.system.fileSystem)).toEqual([
{ kind: 'directory', name: 'root', children: [] },
{
kind: 'directory',
name: '/',
children: [
{
kind: 'directory',
name: null,
children: [{ kind: 'file', name: 'a.txt', type: 'txt', content: 'hallo', children: [] }],
},
],
},
]);
expect(warnings).toEqual(['node 2: file system: insert on a node is not read']);
});
it("gives a modem Java's defaults and keeps an address the file sets to null", () => {
const { nodes } = topologyOf(`<object class="java.util.LinkedList">
${nodeItem('<object class="filius.hardware.knoten.Modem"/>')}