packages/editor (React 18, Vite) draws a document as Java FILIUS does: Java's default hardware icons, the device button of JSidebarButton, cables as JCablePanel's quadratic curve between the buttons' middles, wireless cables dashed, documentation items below. The surface moves with mouse and finger (the pointer captured only once it moved) and zooms with Ctrl and the wheel, a trackpad pinch, two fingers and the zoom bar; the first view shows all. German and English catalogs with Java's device names, the dashboard's colour tokens, the bridge with its origin check. packages/doc gets the drawing's light view of a document. Java's Switch.cloud (the Internet in the examples) was lost on reading; it is now read, kept in the model and written, with FlsOracle and the corpus, written and exported checks renewed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
619 lines
30 KiB
TypeScript
619 lines
30 KiB
TypeScript
// The topology of every corpus file must be what Java FILIUS loads
|
|
// (corpus.java.json, tools/java-oracle): each device with class, name,
|
|
// 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';
|
|
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
decodeJavaBeans,
|
|
readFlsArchive,
|
|
readTopology,
|
|
type FlsApplication,
|
|
type FlsDhcpServer,
|
|
type FlsFileTree,
|
|
type FlsModem,
|
|
type FlsNodeClass,
|
|
type FlsTopology,
|
|
type JavaEmailAccount,
|
|
} from '../src/index.js';
|
|
import { EXPECTED_WARNINGS } from './expectedWarnings.js';
|
|
import { SYSTEM_SOFTWARE_CLASS } from '../src/beanCapture.js';
|
|
import type { BeanValue } from '../src/javaBeansWriter.js';
|
|
import { hasOwnApplications, OWN_APPLICATION_FILES, withOwnApplicationsByClass } from './ownApplications.js';
|
|
import { javaEmail, javaEmailAccount } from './javaEmailNames.js';
|
|
|
|
const corpusRoot = join(dirname(fileURLToPath(import.meta.url)), '../../../corpus');
|
|
const javaView = JSON.parse(readFileSync(join(corpusRoot, 'corpus.java.json'), 'utf8')) as Record<string, unknown>;
|
|
|
|
function flsFiles(directory: string): string[] {
|
|
return readdirSync(directory)
|
|
.flatMap((name) => {
|
|
const path = join(directory, name);
|
|
return statSync(path).isDirectory() ? flsFiles(path) : name.endsWith('.fls') ? [path] : [];
|
|
})
|
|
.sort();
|
|
}
|
|
|
|
/**
|
|
* The palette label Java shows: not read from the file but set from the
|
|
* class while loading (setTyp(holeHardwareTyp())), in German.
|
|
*/
|
|
const TYPE_LABELS: Record<FlsNodeClass, string> = {
|
|
Rechner: 'Rechner',
|
|
Notebook: 'Notebook',
|
|
Switch: 'Switch / WLAN',
|
|
Vermittlungsrechner: 'Vermittlungsrechner',
|
|
Gateway: 'Heimrouter',
|
|
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 };
|
|
}
|
|
}
|
|
|
|
/** FlsOracle.applications' shape: Java's property names, sorted by key. */
|
|
function isSystemSoftwareMark(value: BeanValue): boolean {
|
|
return value !== null && typeof value === 'object' && value.kind === 'object' && value.className === SYSTEM_SOFTWARE_CLASS;
|
|
}
|
|
|
|
function javaApplications(applications: FlsApplication[]): unknown[] {
|
|
return [...applications]
|
|
.sort((lhs, rhs) => ((lhs.key ?? '') < (rhs.key ?? '') ? -1 : 1))
|
|
.map((application) => ({
|
|
key: application.key,
|
|
class: application.javaClass,
|
|
// The reader's applications are its node's: each file gives them its system software.
|
|
onItsSystem: true,
|
|
...(application.isActive === undefined ? {} : { aktiv: application.isActive, port: application.port }),
|
|
...(application.targetPort === undefined
|
|
? {}
|
|
: { zielIPAdresse: application.targetAddress, zielPort: application.targetPort }),
|
|
...(application.recursiveResolutionEnabled === undefined
|
|
? {}
|
|
: { recursiveResolutionEnabled: application.recursiveResolutionEnabled }),
|
|
...(application.useVirtualHosts === undefined ? {} : { useVHost: application.useVirtualHosts }),
|
|
...(application.maxPeers === undefined ? {} : { maxTeilnehmerZahl: application.maxPeers }),
|
|
...(application.firewall === undefined
|
|
? {}
|
|
: {
|
|
activated: application.firewall.activated,
|
|
defaultPolicy: application.firewall.defaultPolicy,
|
|
dropICMP: application.firewall.dropICMP,
|
|
filterSYNSegmentsOnly: application.firewall.filterSYNSegmentsOnly,
|
|
filterUdp: application.firewall.filterUdp,
|
|
rules: application.firewall.rules.map((rule) => [
|
|
rule.srcIP,
|
|
rule.srcMask,
|
|
rule.destIP,
|
|
rule.destMask,
|
|
rule.port,
|
|
rule.protocol,
|
|
rule.action,
|
|
]),
|
|
}),
|
|
...(application.mailDomain === undefined
|
|
? {}
|
|
: {
|
|
mailDomain: application.mailDomain,
|
|
beanAccounts: application.beanAccounts!.map(javaEmailAccount),
|
|
fileAccounts: javaFileAccounts(application.fileAccounts!),
|
|
}),
|
|
...(application.received === undefined
|
|
? {}
|
|
: {
|
|
received: application.received.map(javaEmail),
|
|
sent: application.sent!.map(javaEmail),
|
|
drafts: application.drafts!.map(javaEmail),
|
|
addressBook: application.addressBook!.map((contact) => [
|
|
contact.name,
|
|
contact.firstName,
|
|
contact.street,
|
|
contact.houseNumber,
|
|
contact.postalCode,
|
|
contact.city,
|
|
contact.email,
|
|
contact.phone,
|
|
]),
|
|
fileAccount: application.fileAccount === null ? null : javaEmailAccount(application.fileAccount!),
|
|
}),
|
|
}));
|
|
}
|
|
|
|
function javaFileAccounts(accounts: JavaEmailAccount[] | null): unknown {
|
|
return accounts === null ? null : accounts.map(javaEmailAccount);
|
|
}
|
|
|
|
/** Java's property names, as FlsOracle.java writes them. */
|
|
function javaDhcpServer(server: FlsDhcpServer) {
|
|
return {
|
|
aktiv: server.isActive,
|
|
untergrenze: server.lowerBound,
|
|
obergrenze: server.upperBound,
|
|
gatewayip: server.gateway,
|
|
dnsserverip: server.dnsServer,
|
|
ownSettings: server.useOwnSettings,
|
|
staticAssignedAddresses: server.staticAssignedAddresses.map((entry) => `${entry.mac} ${entry.ip}`),
|
|
};
|
|
}
|
|
|
|
function javaModem(modem: FlsModem) {
|
|
return { mode: modem.mode, port: modem.port, ipAdresse: modem.ipAddress };
|
|
}
|
|
|
|
/** The topology in the shape FlsOracle.java writes, less what Swing computes (label width and height). */
|
|
function asJavaView(topology: FlsTopology, javaBounds: (number[] | null)[]) {
|
|
return {
|
|
success: true,
|
|
nodes: topology.nodes.map((node, index) => ({
|
|
class: node.javaClass,
|
|
name: node.name,
|
|
label: TYPE_LABELS[node.javaClass],
|
|
bounds: [node.x, node.y, ...(javaBounds[index]?.slice(2) ?? [])],
|
|
...(node.javaClass === 'Rechner' || node.javaClass === 'Notebook'
|
|
? { useIpAsName: node.useIpAsName, useMacAsName: node.useMacAsName }
|
|
: {}),
|
|
...(node.javaClass === 'Switch' ? { cloud: node.cloud } : {}),
|
|
ports:
|
|
node.javaClass === 'Switch' || node.javaClass === 'Modem'
|
|
? Array.from({ length: node.portCount }, () => ({}))
|
|
: node.interfaces.map((nic) => ({ ...nic, mac: nic.mac ?? null })),
|
|
system: {
|
|
...node.system,
|
|
...('ssid' in node.system ? { ssid: node.system.ssid ?? null } : {}),
|
|
...(node.system.dhcpServer === undefined
|
|
? {}
|
|
: {
|
|
dhcpServer: javaDhcpServer(node.system.dhcpServer),
|
|
}),
|
|
...(node.system.modem === undefined ? {} : { modem: javaModem(node.system.modem) }),
|
|
...(node.system.fileSystem === undefined ? {} : { fileSystem: javaFileTree(node.system.fileSystem) }),
|
|
...('templateFiles' in node.system ? { templateFiles: undefined } : {}),
|
|
...(node.system.applications === undefined
|
|
? {}
|
|
: {
|
|
applications: [
|
|
...(javaApplications(node.system.applications) as { key: string | null }[]),
|
|
...(node.system.ownApplications ?? []).map((application) => ({
|
|
key: application.key,
|
|
class: application.javaClass,
|
|
onItsSystem: application.bean.statements?.some(
|
|
(statement) => 'set' in statement && statement.set === 'systemSoftware' && isSystemSoftwareMark(statement.value),
|
|
),
|
|
})),
|
|
].sort((lhs, rhs) => ((lhs.key ?? '') < (rhs.key ?? '') ? -1 : 1)),
|
|
}),
|
|
...('ownApplications' in node.system ? { ownApplications: undefined } : {}),
|
|
...(node.system.firewall === undefined
|
|
? {}
|
|
: {
|
|
firewall: {
|
|
...node.system.firewall,
|
|
rules: node.system.firewall.rules.map((rule) => [
|
|
rule.srcIP,
|
|
rule.srcMask,
|
|
rule.destIP,
|
|
rule.destMask,
|
|
rule.port,
|
|
rule.protocol,
|
|
rule.action,
|
|
]),
|
|
},
|
|
}),
|
|
},
|
|
})),
|
|
cables: topology.links.map((link) => ({
|
|
ends: link.ends.map((end) => [end.node, end.port]),
|
|
wireless: link.wireless,
|
|
panel: link.panel,
|
|
})),
|
|
documentation: topology.documentation.map((item) => ({
|
|
type: item.type,
|
|
text: item.text,
|
|
bounds: [item.x, item.y, item.width, item.height],
|
|
color: item.color,
|
|
font: item.font === null ? null : [item.font.name, item.font.size, item.font.style],
|
|
})),
|
|
};
|
|
}
|
|
|
|
type JavaTree = { children: unknown[] };
|
|
|
|
/**
|
|
* The files a router's or home router's constructor makes, as Java loaded
|
|
* each one: they come first in its root, what the file adds follows. Each
|
|
* router's are cut off here and collected, and must be the same for every
|
|
* router of a class across the corpus.
|
|
*/
|
|
const templateChildren = new Map<string, Set<string>>();
|
|
|
|
function withoutTemplateFiles(view: unknown, topology: FlsTopology): unknown {
|
|
const { nodes, ...rest } = view as { nodes: { class: string; system: Record<string, unknown> }[] };
|
|
return {
|
|
...rest,
|
|
nodes: nodes.map((node, index) => {
|
|
const ours = topology.nodes[index]?.system;
|
|
if ((node.class !== 'Vermittlungsrechner' && node.class !== 'Gateway') || ours?.templateFiles !== true) return node;
|
|
const tree = node.system.fileSystem as JavaTree;
|
|
const added = ours.fileSystem?.children.length ?? 0;
|
|
const template = tree.children.slice(0, tree.children.length - added);
|
|
const seen = templateChildren.get(node.class) ?? new Set<string>();
|
|
seen.add(JSON.stringify(template));
|
|
templateChildren.set(node.class, seen);
|
|
return {
|
|
...node,
|
|
system: { ...node.system, fileSystem: { ...tree, children: tree.children.slice(tree.children.length - added) } },
|
|
};
|
|
}),
|
|
};
|
|
}
|
|
|
|
describe('corpus: the topology as Java loads it', () => {
|
|
for (const file of flsFiles(corpusRoot)) {
|
|
const name = relative(corpusRoot, file);
|
|
it(name, () => {
|
|
const values = decodeJavaBeans(readFlsArchive(new Uint8Array(readFileSync(file))).configurationXml);
|
|
const topology = readTopology(values);
|
|
const expected = javaView[name] as { nodes: { bounds: number[] | null }[] };
|
|
|
|
expect(asJavaView(topology, expected.nodes.map((node) => node.bounds))).toEqual(
|
|
withoutTemplateFiles(withOwnApplicationsByClass(expected), topology),
|
|
);
|
|
expect(topology.warnings).toEqual(EXPECTED_WARNINGS[name] ?? []);
|
|
});
|
|
}
|
|
|
|
it('leaves out own applications only where Java installs some', () => {
|
|
const files = flsFiles(corpusRoot).map((file) => relative(corpusRoot, file));
|
|
expect(files.filter((name) => hasOwnApplications(javaView[name]))).toEqual(OWN_APPLICATION_FILES);
|
|
});
|
|
|
|
it("a router's constructor makes the same files in every corpus file", () => {
|
|
for (const javaClass of ['Vermittlungsrechner', 'Gateway']) {
|
|
const templates = [...(templateChildren.get(javaClass) ?? [])].map((text) => JSON.parse(text) as { directory?: string }[]);
|
|
expect(templates, javaClass).toHaveLength(1);
|
|
expect(templates[0]!.map((child) => child.directory), javaClass).toEqual(['webserver']);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ── Rules the corpus does not exercise ─────────────────────────────────────
|
|
|
|
function doc(body: string): string {
|
|
return `<?xml version="1.0" encoding="UTF-8"?>\n<java version="17" class="java.beans.XMLDecoder">\n${body}\n</java>\n`;
|
|
}
|
|
|
|
function topologyOf(body: string): FlsTopology {
|
|
return readTopology(decodeJavaBeans(doc(body)));
|
|
}
|
|
|
|
function nodeItem(device: string, extra = ''): string {
|
|
return `<void method="add"><object class="filius.gui.netzwerksicht.GUIKnotenItem">
|
|
<void property="knoten">${device}</void>${extra}</object></void>`;
|
|
}
|
|
|
|
describe('readTopology rules', () => {
|
|
it('passes addresses through Java setIp: canonical, null when invalid, unchanged when null', () => {
|
|
const { nodes } = topologyOf(`<string>Filius version: 2.9.4 (1.1.2026)</string>
|
|
<object class="java.util.LinkedList">${nodeItem(`<object class="filius.hardware.knoten.Vermittlungsrechner">
|
|
<void property="netzwerkInterfaces">
|
|
<void index="0"><void property="ip"><string>010.0..0.1</string></void>
|
|
<void property="subnetzMaske"><string>255.255.255.256</string></void></void>
|
|
<void method="add"><object class="filius.hardware.NetzwerkInterface">
|
|
<void property="ip"><null/></void><void property="mac"><string>0a:0b:0c:0d:0e:0f</string></void>
|
|
</object></void>
|
|
</void></object>`)}</object>`);
|
|
|
|
expect(nodes[0]!.interfaces).toEqual([
|
|
{ ip: '10.0.0.1', mask: null, mac: undefined, gateway: '', dns: '', wireless: false },
|
|
{ ip: '192.168.0.10', mask: '255.255.255.0', mac: '0A:0B:0C:0D:0E:0F', gateway: '', dns: '', wireless: false },
|
|
]);
|
|
});
|
|
|
|
it("gives only a Gateway's own first interface the WAN address, not one the file creates", () => {
|
|
const own = topologyOf(`<object class="java.util.LinkedList">${nodeItem(
|
|
'<object class="filius.hardware.knoten.Gateway"/>',
|
|
)}</object>`);
|
|
const replaced = topologyOf(`<object class="java.util.LinkedList">${nodeItem(`<object class="filius.hardware.knoten.Gateway">
|
|
<void property="netzwerkInterfaces"><object class="java.util.LinkedList">
|
|
<void method="add"><object class="filius.hardware.NetzwerkInterface"/></void>
|
|
</object></void></object>`)}</object>`);
|
|
|
|
expect(own.nodes[0]!.interfaces.map((nic) => [nic.ip, nic.mask])).toEqual([
|
|
['42.0.0.10', '255.0.0.0'],
|
|
['192.168.0.10', '255.255.255.0'],
|
|
]);
|
|
expect(own.nodes[0]!.name).toBe('Heimrouter');
|
|
expect(replaced.nodes[0]!.interfaces.map((nic) => nic.ip)).toEqual(['192.168.0.10']);
|
|
});
|
|
|
|
it('places a device without bounds at 0,0 instead of leaving it out', () => {
|
|
const { nodes, warnings } = topologyOf(
|
|
`<object class="java.util.LinkedList">${nodeItem('<object class="filius.hardware.knoten.Notebook"/>')}</object>`,
|
|
);
|
|
|
|
expect(nodes.map((node) => [node.javaClass, node.x, node.y, node.portCount])).toEqual([['Notebook', 0, 0, 1]]);
|
|
expect(warnings).toEqual([]);
|
|
});
|
|
|
|
it('leaves out an unknown device and a cable to a port no device owns, and says so', () => {
|
|
const { nodes, links, warnings } = topologyOf(`<object class="java.util.LinkedList">
|
|
${nodeItem('<object class="filius.hardware.knoten.Drucker"/>')}
|
|
${nodeItem(`<object class="filius.hardware.knoten.Switch"><void property="anschluesse">
|
|
<void id="Port0" index="0"/><void id="Port1" index="1"/></void></object>`)}
|
|
</object>
|
|
<object class="java.util.LinkedList">
|
|
<void method="add"><object class="filius.gui.netzwerksicht.GUIKabelItem"><void property="dasKabel">
|
|
<object class="filius.hardware.Kabel"><object idref="Port0"/><object idref="Port1"/></object></void></object></void>
|
|
<void method="add"><object class="filius.gui.netzwerksicht.GUIKabelItem"><void property="dasKabel">
|
|
<object class="filius.hardware.Kabel"><object idref="Port0"/><object class="filius.hardware.Port"/></object></void></object></void>
|
|
</object>`);
|
|
|
|
expect(nodes.map((node) => node.javaClass)).toEqual(['Switch']);
|
|
expect(links).toEqual([{ ends: [{ node: 0, port: 0 }, { node: 0, port: 1 }], wireless: false, panel: [null, null] }]);
|
|
expect(warnings).toEqual([
|
|
'node 1: unknown device class filius.hardware.knoten.Drucker, left out',
|
|
'cable 2: an end is not a port of a device, left out',
|
|
]);
|
|
});
|
|
|
|
it('reads the lists by position and only when the first element fits, as netzwerkLaden does', () => {
|
|
// No version line (files before 1.3.0); an empty cable list still takes its place.
|
|
const topology = topologyOf(`<object class="java.util.LinkedList">${nodeItem(
|
|
'<object class="filius.hardware.knoten.Rechner"/>',
|
|
)}</object>
|
|
<object class="java.util.LinkedList"/>
|
|
<object class="java.util.LinkedList"><void method="add"><object class="filius.gui.netzwerksicht.GUIDocuItem">
|
|
<void property="type"><int>1</int></void></object></void></object>`);
|
|
const wrongFirst = topologyOf(`<object class="java.util.LinkedList">
|
|
<void method="add"><string>not a node</string></void>
|
|
${nodeItem('<object class="filius.hardware.knoten.Rechner"/>')}</object>`);
|
|
|
|
expect(topology.versionLine).toBeNull();
|
|
expect(topology.nodes).toHaveLength(1);
|
|
expect(topology.documentation.map((item) => item.type)).toEqual([1]);
|
|
expect(wrongFirst.nodes).toEqual([]);
|
|
});
|
|
|
|
it("reads a router's firewall wherever the file reaches it, with Java's defaults for what it leaves out", () => {
|
|
// Settings under the installed application and under the web server's
|
|
// plug-in belong to the one firewall the constructor made.
|
|
const { nodes } = topologyOf(`<object class="java.util.LinkedList">${nodeItem(`<object class="filius.hardware.knoten.Vermittlungsrechner">
|
|
<void property="systemSoftware"><void property="installierteAnwendungen">
|
|
<void method="get"><string>filius.software.firewall.Firewall</string>
|
|
<void property="allowRelatedPackets"><boolean>false</boolean></void>
|
|
<void property="ruleset">
|
|
<void method="add"><object class="filius.software.firewall.FirewallRule" id="FirewallRule0">
|
|
<void class="filius.software.firewall.FirewallRule" method="getField"><string>srcIP</string>
|
|
<void method="set"><object idref="FirewallRule0"/><string>10.0.0.0</string></void></void>
|
|
</object></void>
|
|
<void method="add"><object class="filius.software.firewall.FirewallRule">
|
|
<string>1.1.1.1</string><string>255.255.255.255</string><string></string><string></string>
|
|
<int>53</int><short>17</short><short>1</short>
|
|
</object></void>
|
|
</void>
|
|
</void>
|
|
<void method="get"><string>filius.software.www.WebServer</string>
|
|
<void property="plugins"><void method="get"><string>konfig</string>
|
|
<void property="firewall"><void property="activated"><boolean>true</boolean></void></void>
|
|
</void></void>
|
|
</void>
|
|
</void></void></object>`)}</object>`);
|
|
|
|
expect(nodes[0]!.system.firewall).toEqual({
|
|
activated: true,
|
|
defaultPolicy: 0,
|
|
dropICMP: false,
|
|
filterSYNSegmentsOnly: false,
|
|
filterUdp: true,
|
|
rules: [
|
|
{ srcIP: '10.0.0.0', srcMask: '', destIP: '', destMask: '', port: -1, protocol: 6, action: 0 },
|
|
{ srcIP: '1.1.1.1', srcMask: '255.255.255.255', destIP: '', destMask: '', port: 53, protocol: 17, action: 1 },
|
|
],
|
|
});
|
|
});
|
|
|
|
it('splits static DHCP assignments at Java whitespace and leaves out only a broken one', () => {
|
|
// U+3000 separates, U+00A0 does not; the third entry has no IP. Java
|
|
// throws there and never reads the fourth — an error not copied.
|
|
const { nodes, warnings } = topologyOf(`<object class="java.util.LinkedList">${nodeItem(`<object class="filius.hardware.knoten.Rechner">
|
|
<void property="systemSoftware"><void property="DHCPServer">
|
|
<void property="staticAssignedAddresses"><object class="java.util.ArrayList">
|
|
<void method="add"><string>0A:00:00:00:00:01<char code="#3000"/>10.0.0.1<char code="#9"/>extra</string></void>
|
|
<void method="add"><string>0A:00:00:00:00:02 10.0.0.2<char code="#A0"/>x</string></void>
|
|
<void method="add"><string>0A:00:00:00:00:03</string></void>
|
|
<void method="add"><string>0A:00:00:00:00:04 10.0.0.4</string></void>
|
|
</object></void>
|
|
</void></void></object>`)}</object>`);
|
|
|
|
expect(nodes[0]!.system.dhcpServer).toEqual({
|
|
isActive: false,
|
|
lowerBound: '0.0.0.0',
|
|
upperBound: '0.0.0.0',
|
|
gateway: '0.0.0.0',
|
|
dnsServer: '0.0.0.0',
|
|
useOwnSettings: false,
|
|
staticAssignedAddresses: [
|
|
{ mac: '0A:00:00:00:00:01', ip: '10.0.0.1' },
|
|
{ mac: '0A:00:00:00:00:02', ip: '10.0.0.2\u00A0x' },
|
|
{ mac: '0A:00:00:00:00:04', ip: '10.0.0.4' },
|
|
],
|
|
});
|
|
expect(warnings).toEqual([
|
|
'node 1: DHCPServer: static assignment "0A:00:00:00:00:03" has no MAC and IP, left out',
|
|
]);
|
|
});
|
|
|
|
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("reads what a router's file adds to the constructor's files, or a root of its own", () => {
|
|
const folder = (name: string) =>
|
|
`<void method="add"><object class="javax.swing.tree.DefaultMutableTreeNode"><string>${name}</string></object></void>`;
|
|
const { nodes } = topologyOf(`<object class="java.util.LinkedList">
|
|
${nodeItem('<object class="filius.hardware.knoten.Vermittlungsrechner"/>')}
|
|
${nodeItem(`<object class="filius.hardware.knoten.Gateway"><void property="systemSoftware"><void property="dateisystem">
|
|
<void property="arbeitsVerzeichnis">${folder('www.conf')}</void>
|
|
</void></void></object>`)}
|
|
${nodeItem(`<object class="filius.hardware.knoten.Vermittlungsrechner"><void property="systemSoftware"><void property="dateisystem">
|
|
<void property="arbeitsVerzeichnis"><object class="javax.swing.tree.DefaultMutableTreeNode"><string>root</string>${folder('own')}</object></void>
|
|
</void></void></object>`)}
|
|
${nodeItem(`<object class="filius.hardware.knoten.Gateway"><void property="systemSoftware">
|
|
<void property="dateisystem"><object class="filius.software.system.Dateisystem"/></void>
|
|
</void></object>`)}</object>`);
|
|
const directory = (name: string, children: FlsFileTree[] = []): FlsFileTree => ({ kind: 'directory', name, children });
|
|
|
|
expect(nodes.map((node) => [node.system.fileSystem, node.system.templateFiles])).toEqual([
|
|
[directory('root'), true],
|
|
[directory('root', [directory('www.conf')]), true],
|
|
[directory('root', [directory('own')]), false],
|
|
[directory('root'), false],
|
|
]);
|
|
});
|
|
|
|
it("gives installed applications Java's measured defaults for what the file leaves out", () => {
|
|
const put = (className: string, body = '') =>
|
|
`<void method="put"><string>${className}</string><object class="${className}">${body}</object></void>`;
|
|
const { nodes } = topologyOf(`<object class="java.util.LinkedList">${nodeItem(`<object class="filius.hardware.knoten.Rechner">
|
|
<void property="systemSoftware"><void property="installierteAnwendungen">
|
|
${put('filius.software.www.WebServer')}
|
|
${put('filius.software.clientserver.ClientBaustein')}
|
|
${put('filius.software.dateiaustausch.PeerToPeerAnwendung')}
|
|
${put('filius.software.firewall.Firewall')}
|
|
${put('filius.software.dns.DNSServer', '<void property="aktiv"><boolean>true</boolean></void><void property="port"><int>5353</int></void>')}
|
|
${put('filius.software.lokal.Terminal')}
|
|
</void></void></object>`)}</object>`);
|
|
const byClass = new Map(nodes[0]!.system.applications!.map((application) => [application.javaClass, application]));
|
|
|
|
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 });
|
|
// 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']);
|
|
});
|
|
|
|
it('reads the e-mail beans through their setters: null names, string senders, a checked domain', () => {
|
|
const put = (className: string, body = '') =>
|
|
`<void method="put"><string>${className}</string><object class="${className}">${body}</object></void>`;
|
|
const { nodes } = topologyOf(`<object class="java.util.LinkedList">${nodeItem(`<object class="filius.hardware.knoten.Rechner">
|
|
<void property="systemSoftware"><void property="installierteAnwendungen">
|
|
${put('filius.software.email.EmailServer', `
|
|
<void property="mailDomain"><string>a_b.de</string></void>
|
|
<void property="listeBenutzerkonten"><void method="add"><object class="filius.software.email.EmailKonto">
|
|
<void property="benutzername"><string>bob</string></void>
|
|
<void property="nachname"><null/></void>
|
|
<void property="nachrichten"><void method="add"><object class="filius.software.email.Email">
|
|
<void property="absender"><string> Alice <alice@senior.de> </string></void>
|
|
</object></void></void>
|
|
</object></void></void>`)}
|
|
</void></void></object>`)}</object>`);
|
|
const server = nodes[0]!.system.applications![0]!;
|
|
|
|
// setMailDomain keeps "filius.de" for a domain that is not a domain name.
|
|
expect(server).toMatchObject({ isActive: false, mailDomain: 'filius.de' });
|
|
expect(server.beanAccounts![0]).toMatchObject({ username: 'bob', lastName: '', firstName: '' });
|
|
expect(server.beanAccounts![0]!.messages[0]!.sender).toEqual({ name: 'Alice', mailAddress: 'alice@senior.de' });
|
|
});
|
|
|
|
it("reads an older file's kontoListe, which FILIUS 2.14 ignores, in the order of its entries", () => {
|
|
const legacyAccount = (username: string) =>
|
|
`<void method="put"><string>${username}@senior.de</string><object class="filius.software.email.EmailKonto">
|
|
<void property="benutzername"><string>${username}</string></void>
|
|
<void property="pop3port"><string>110</string></void>
|
|
</object></void>`;
|
|
const { nodes } = topologyOf(`<object class="java.util.LinkedList">${nodeItem(`<object class="filius.hardware.knoten.Rechner">
|
|
<void property="systemSoftware"><void property="installierteAnwendungen">
|
|
<void method="put"><string>filius.software.email.EmailAnwendung</string><object class="filius.software.email.EmailAnwendung">
|
|
<void property="kontoListe">${legacyAccount('rechner3')}${legacyAccount('rechner4')}</void>
|
|
</object></void>
|
|
</void></void></object>`)}</object>`);
|
|
const client = nodes[0]!.system.applications![0]!;
|
|
|
|
expect(client.fileAccount).toBeNull();
|
|
expect(client.legacyAccounts!.map((account) => [account.username, account.pop3Port])).toEqual([
|
|
['rechner3', '110'],
|
|
['rechner4', '110'],
|
|
]);
|
|
});
|
|
|
|
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"/>')}
|
|
${nodeItem(`<object class="filius.hardware.knoten.Modem"><void property="systemSoftware">
|
|
<void property="ipAdresse"><null/></void><void property="mode"><int>1</int></void>
|
|
</void></object>`)}</object>`);
|
|
|
|
expect(nodes.map((node) => node.system.modem)).toEqual([
|
|
{ mode: 2, port: 12_345, ipAddress: 'localhost' },
|
|
{ mode: 1, port: 12_345, ipAddress: null },
|
|
]);
|
|
});
|
|
|
|
it("reads colors as Java's constructors do and keeps an invalid one unset", () => {
|
|
const colors = ['<int>16711935</int>', '<int>1</int><int>2</int><int>3</int>', '<int>1</int><int>2</int><int>300</int><int>4</int>'];
|
|
const { documentation, warnings } = topologyOf(`<object class="java.util.LinkedList"/><object class="java.util.LinkedList"/>
|
|
<object class="java.util.LinkedList">${colors
|
|
.map(
|
|
(args) => `<void method="add"><object class="filius.gui.netzwerksicht.GUIDocuItem">
|
|
<void property="color"><object class="java.awt.Color">${args}</object></void>
|
|
<void property="font"><object class="java.awt.Font"><string>Dialog</string><int>5</int><int>14</int></object></void>
|
|
</object></void>`,
|
|
)
|
|
.join('')}</object>`);
|
|
|
|
expect(documentation.map((item) => item.color)).toEqual([[255, 0, 255, 255], [1, 2, 3, 255], null]);
|
|
expect(documentation[0]!.font).toEqual({ name: 'Dialog', size: 14, style: 0 });
|
|
expect(warnings).toEqual(['documentation item: color component outside 0…255, left unset (as in Java)']);
|
|
});
|
|
});
|