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:
@@ -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)));
|
||||
|
||||
@@ -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"/>')}
|
||||
|
||||
Reference in New Issue
Block a user