// Every corpus file, read and written again: Java FILIUS loads the written // file (tools/java-oracle, written.java.json) to the same topology as the // original (corpus.java.json) — devices, names, positions, interfaces, // system software, files, applications with their e-mail accounts, // messages and address books, cables and documentation — up to one Java // error the writer does not copy (RECIPIENTS_AS_STRINGS). Java against Java: no mapping of this package stands // between the two. // // UPDATE_WRITTEN=1 writes the files to test/written/ (not checked in); the // Java oracle then loads them and records each file's SHA-256, which must // match what the writer makes now. import { createHash } from 'node:crypto'; import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } 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, writeFlsArchive } from '../src/index.js'; import type { JavaEmail } from '../src/javaEmail.js'; import { writeTopologyXml, type TopologyToWrite } from '../src/topologyWriter.js'; const here = dirname(fileURLToPath(import.meta.url)); const corpusRoot = join(here, '../../../corpus'); const writtenRoot = join(here, 'written'); function flsFiles(directory: string): string[] { return readdirSync(directory).flatMap((name) => { const path = join(directory, name); if (statSync(path).isDirectory()) return flsFiles(path); return name.endsWith('.fls') ? [path] : []; }); } type JavaNode = { class: string; name: string | null; bounds: number[]; useIpAsName: boolean; useMacAsName: boolean; ports: unknown; system: Record; }; type JavaFile = { success: boolean; nodes: JavaNode[]; cables: unknown[]; documentation: unknown[]; sha256?: string }; const original = JSON.parse(readFileSync(join(corpusRoot, 'corpus.java.json'), 'utf8')) as Record; const written = JSON.parse(readFileSync(join(here, 'written.java.json'), 'utf8')) as Record; /** * The Java error the writer does not copy: older files keep e-mail * recipients as strings, which Java puts into its typed lists unchecked (its * e-mail window then fails on them). The writer writes the AddressEntry * Java's own AddressEntry(String) makes of each — the oracle reports it * next to the string — and leaves a null recipient out. */ const RECIPIENTS_AS_STRINGS = ['java-examples/man-in-the-middle.fls']; /** Java's view of the original as the writer means it: each recipient as Java reads its string, none null. */ function withRecipientsRead(value: unknown, key = ''): unknown { if (Array.isArray(value)) { if (key === 'empfaenger' || key === 'cc' || key === 'bcc') { return value.filter((entry) => entry !== null).map((entry) => (entry[0] === 'string' ? entry[2] : entry)); } return value.map((item) => withRecipientsRead(item)); } if (value !== null && typeof value === 'object') { return Object.fromEntries(Object.entries(value).map(([name, item]) => [name, withRecipientsRead(item, name)])); } return value; } /** * The topology of Java's view: the label's position, not its size, which * Java works out itself. */ function topologyOf(view: JavaFile) { return { success: view.success, nodes: view.nodes.map((node) => ({ class: node.class, name: node.name, position: node.bounds.slice(0, 2), useIpAsName: node.useIpAsName, useMacAsName: node.useMacAsName, ports: node.ports, system: node.system, })), cables: view.cables, documentation: view.documentation, }; } describe('the topology written again, as Java loads it', () => { for (const file of flsFiles(corpusRoot)) { const name = relative(corpusRoot, file); it(name, () => { const topology = readTopology(decodeJavaBeans(readFlsArchive(new Uint8Array(readFileSync(file))).configurationXml)); const archive = writeFlsArchive(new TextEncoder().encode(writeTopologyXml(topology))); if (process.env.UPDATE_WRITTEN === '1') { mkdirSync(dirname(join(writtenRoot, name)), { recursive: true }); writeFileSync(join(writtenRoot, name), archive); } const java = written[name]; expect(java?.sha256, 'written.java.json is not about this output: UPDATE_WRITTEN=1, then npm run oracle:java').toBe( createHash('sha256').update(archive).digest('hex'), ); expect(topologyOf(java!)).toEqual(topologyOf(withRecipientsRead(original[name]!) as JavaFile)); }); } it('departs from Java only where it keeps recipients as strings', () => { const changed = flsFiles(corpusRoot) .map((file) => relative(corpusRoot, file)) .filter((name) => JSON.stringify(withRecipientsRead(original[name])) !== JSON.stringify(original[name])); expect(changed).toEqual(RECIPIENTS_AS_STRINGS); }); }); /** * An e-mail with a sender without address, recipients as entries, as the * string an older file keeps (read as AddressEntry(String) reads it) and * null (left out), a text with CR, and a date. */ const SYNTHETIC_MAIL: JavaEmail = { sender: { name: 'Anna', mailAddress: null }, to: [{ name: null, mailAddress: 'ben@schule.de' }, 'Carla ', null], cc: [{ name: 'Dora', mailAddress: 'dora@schule.de' }], bcc: ['eva@schule.de'], subject: 'Hallo', text: 'Zeile 1\r\nZeile 2', dateReceived: '1727690000000', isNew: true, isDeleted: true, isSent: false, }; /** SYNTHETIC_MAIL as the oracle describes it after Java loaded it. */ function javaMail(changes: Record = {}) { return { absender: ['Anna', null], empfaenger: [[null, 'ben@schule.de'], ['Carla', 'carla@schule.de']], cc: [['Dora', 'dora@schule.de']], bcc: [[null, 'eva@schule.de']], betreff: 'Hallo', text: 'Zeile 1\r\nZeile 2', dateReceived: '1727690000000', neu: true, delete: true, versendet: false, ...changes, }; } /** Text a file's content may hold: markup, CR and CRLF, a tab, beyond the BMP. */ const FILE_CONTENT = '

a & b

\r\nline\rend\ttab Grüße 😀\n'; /** * What the corpus does not have: a name set to null, useMACAsName, fewer * ports than the constructor makes, more interfaces; a PC's SSID, routes and * DHCP server with static assignments; a router's firewall switched on, with * every flag away from its default and rules of every protocol, one with a * null address; a modem's own port and address. */ const SYNTHETIC: TopologyToWrite = { nodes: [ { javaClass: 'Rechner', name: null, x: 10, y: 20, useIpAsName: false, useMacAsName: true, interfaces: [{ ip: '10.0.0.1', mask: '255.0.0.0', mac: 'AA:BB:CC:DD:EE:01', gateway: null, dns: '10.0.0.53' }], portCount: 1, system: { ssid: 'Lab', manualRoutes: [['10.1.0.0', '255.255.0.0', '10.0.0.254', '10.0.0.1']], dhcpClient: true, dhcpServer: { isActive: true, lowerBound: '10.0.0.100', upperBound: '10.0.0.199', gateway: '10.0.0.254', dnsServer: '10.0.0.53', useOwnSettings: true, staticAssignedAddresses: [{ mac: 'AA:BB:CC:DD:EE:77', ip: '10.0.0.77' }], }, fileSystem: { kind: 'directory', name: '/', children: [ { kind: 'directory', name: 'docs', children: [ { kind: 'file', name: 'a.txt', type: 'text', content: FILE_CONTENT, children: [] }, { kind: 'directory', name: null, children: [] }, ], }, { kind: 'file', name: 'untyped', type: null, content: null, children: [] }, { kind: 'directory', name: 'webserver', children: [{ kind: 'file', name: 'index.html', type: 'html', content: '

own

', children: [] }] }, { kind: 'directory', name: 'dns', children: [{ kind: 'file', name: 'hosts', type: null, content: 'a.de. A 3600 10.0.0.1\n', children: [] }] }, ], }, applications: [ { key: 'filius.software.firewall.Firewall', javaClass: 'filius.software.firewall.Firewall', firewall: { activated: false, defaultPolicy: 1, dropICMP: true, filterSYNSegmentsOnly: false, filterUdp: false, rules: [{ srcIP: '10.0.0.0', srcMask: '255.0.0.0', destIP: '', destMask: '', port: 22, protocol: 6, action: 1 }], }, }, { key: 'filius.software.www.WebServer', javaClass: 'filius.software.www.WebServer', isActive: true, port: 8080, useVirtualHosts: true }, { key: 'filius.software.dns.DNSServer', javaClass: 'filius.software.dns.DNSServer', isActive: true, port: 5353, recursiveResolutionEnabled: true, }, { key: 'filius.software.clientserver.ServerBaustein', javaClass: 'filius.software.clientserver.ServerBaustein', isActive: true, port: 7777 }, { key: 'filius.software.clientserver.ClientBaustein', javaClass: 'filius.software.clientserver.ClientBaustein', targetAddress: '10.0.0.9', targetPort: 7777, }, { key: 'filius.software.dateiaustausch.PeerToPeerAnwendung', javaClass: 'filius.software.dateiaustausch.PeerToPeerAnwendung', maxPeers: 7 }, { key: 'filius.software.lokal.Terminal', javaClass: 'filius.software.lokal.Terminal' }, { key: 'filius.software.lokal.TextEditor', javaClass: 'filius.software.lokal.TextEditor' }, { key: 'filius.software.lokal.FileExplorer', javaClass: 'filius.software.lokal.FileExplorer' }, { key: 'filius.software.lokal.ImageViewer', javaClass: 'filius.software.lokal.ImageViewer' }, { key: 'filius.software.www.WebBrowser', javaClass: 'filius.software.www.WebBrowser' }, { key: 'filius.software.email.EmailServer', javaClass: 'filius.software.email.EmailServer', isActive: true, mailDomain: 'schule.de', beanAccounts: [ { username: 'anna', password: 'geheim', lastName: 'Muster', firstName: '', emailAddress: 'anna@schule.de', pop3Server: null, pop3Port: null, smtpServer: null, smtpPort: null, messages: [SYNTHETIC_MAIL], }, ], }, { key: 'filius.software.email.EmailAnwendung', javaClass: 'filius.software.email.EmailAnwendung', received: [SYNTHETIC_MAIL], sent: [{ ...SYNTHETIC_MAIL, isSent: true, isNew: false }], drafts: [{ ...SYNTHETIC_MAIL, subject: null, text: null, dateReceived: null, to: [] }], addressBook: [ { name: 'Muster', firstName: 'Anna', street: null, houseNumber: 12, postalCode: 20095, city: 'Hamburg', email: 'anna@schule.de', phone: null, }, ], }, ], }, }, { javaClass: 'Switch', name: 'S', x: 100, y: 20, useIpAsName: false, useMacAsName: false, interfaces: [], portCount: 3, system: { ssid: 'S-net', retentionTime: 1234 }, }, { javaClass: 'Vermittlungsrechner', name: 'R', x: 200, y: 20, useIpAsName: false, useMacAsName: false, interfaces: ['1', '2', '3'].map((octet) => ({ ip: `10.0.${octet}.1`, mask: '255.255.255.0', mac: `AA:BB:CC:DD:EE:1${octet}`, gateway: '', dns: '', })), portCount: 3, system: { ripEnabled: true, fileSystem: { kind: 'directory', name: 'root', children: [{ kind: 'directory', name: 'www.conf', children: [] }] }, templateFiles: true, firewall: { activated: true, defaultPolicy: 1, dropICMP: true, filterSYNSegmentsOnly: false, filterUdp: false, rules: [ { srcIP: '10.0.1.0', srcMask: '255.255.255.0', destIP: '', destMask: '', port: -1, protocol: -1, action: 1 }, { srcIP: '', srcMask: '', destIP: '10.0.2.5', destMask: '255.255.255.255', port: 80, protocol: 6, action: 0 }, { srcIP: null, srcMask: '', destIP: '', destMask: '', port: 53, protocol: 17, action: 1 }, { srcIP: '', srcMask: '', destIP: '', destMask: '', port: -1, protocol: 1, action: 0 }, ], }, }, }, { javaClass: 'Modem', name: 'M', x: 300, y: 20, useIpAsName: false, useMacAsName: false, interfaces: [], portCount: 1, system: { modem: { mode: 1, port: 4242, ipAddress: '192.168.1.5' } }, }, { javaClass: 'Gateway', name: 'G', x: 400, y: 20, useIpAsName: false, useMacAsName: false, interfaces: ['4', '5'].map((octet) => ({ ip: `10.0.${octet}.1`, mask: '255.255.255.0', mac: `AA:BB:CC:DD:EE:1${octet}`, gateway: '', dns: '', })), portCount: 2, system: { fileSystem: { kind: 'directory', name: 'own', children: [{ kind: 'file', name: 'x', type: 'text', content: 'y', children: [] }] }, templateFiles: false, }, }, ], links: [ { ends: [{ node: 0, port: 0 }, { node: 1, port: 2 }], wireless: false, panel: [0, 1] }, { ends: [{ node: 2, port: 2 }, { node: 1, port: 0 }], wireless: true, panel: [2, 1] }, ], documentation: [{ type: 1, text: null, x: 5, y: 6, width: 70, height: 80, color: null, font: null }], }; /** The DHCP server as every IP stack's constructor leaves it. */ const JAVA_DHCP_DEFAULTS = { aktiv: false, untergrenze: '0.0.0.0', obergrenze: '0.0.0.0', gatewayip: '0.0.0.0', dnsserverip: '0.0.0.0', ownSettings: false, staticAssignedAddresses: [], }; /** A file as the oracle describes it: its content as UTF-16 length and SHA-256 of its UTF-8 bytes. */ function javaFile(name: string, type: string | null, content: string | null) { return { file: name, type, length: content === null ? null : content.length, sha256: content === null ? null : createHash('sha256').update(content, 'utf8').digest('hex'), children: [], }; } /** The files a router's constructor makes, as Java loaded them from the corpus. */ function routerTemplate(): unknown[] { const router = Object.values(original) .flatMap((file) => file.nodes ?? []) .find((node) => node.class === 'Vermittlungsrechner'); return ((router!.system.fileSystem as { children: unknown[] }).children).slice(0, 1); } /** A router's firewall as its constructor leaves it. */ const JAVA_ROUTER_FIREWALL = { activated: false, defaultPolicy: 0, dropICMP: false, filterSYNSegmentsOnly: true, filterUdp: true, rules: [] }; describe('a topology beyond the corpus, as Java loads it', () => { it('keeps what the corpus never sets', () => { const name = 'synthetic/synthetic.fls'; const archive = writeFlsArchive(new TextEncoder().encode(writeTopologyXml(SYNTHETIC))); if (process.env.UPDATE_WRITTEN === '1') { mkdirSync(join(writtenRoot, 'synthetic'), { recursive: true }); writeFileSync(join(writtenRoot, name), archive); } const java = written[name]; expect(java?.sha256, 'written.java.json is not about this output: UPDATE_WRITTEN=1, then npm run oracle:java').toBe( createHash('sha256').update(archive).digest('hex'), ); const plainInterface = (octet: string) => ({ ip: `10.0.${octet}.1`, mask: '255.255.255.0', mac: `AA:BB:CC:DD:EE:1${octet}`, gateway: '', dns: '' }); expect(topologyOf(java!)).toEqual({ success: true, nodes: [ { class: 'Rechner', name: null, position: [10, 20], useIpAsName: false, useMacAsName: true, ports: [{ ip: '10.0.0.1', mask: '255.0.0.0', mac: 'AA:BB:CC:DD:EE:01', gateway: null, dns: '10.0.0.53' }], system: { ssid: 'Lab', manualRoutes: [['10.1.0.0', '255.255.0.0', '10.0.0.254', '10.0.0.1']], dhcpClient: true, dhcpServer: { aktiv: true, untergrenze: '10.0.0.100', obergrenze: '10.0.0.199', gatewayip: '10.0.0.254', dnsserverip: '10.0.0.53', ownSettings: true, staticAssignedAddresses: ['AA:BB:CC:DD:EE:77 10.0.0.77'], }, fileSystem: { directory: '/', children: [ { directory: 'docs', children: [javaFile('a.txt', 'text', FILE_CONTENT), { directory: null, children: [] }] }, javaFile('untyped', null, null), { directory: 'webserver', children: [javaFile('index.html', 'html', '

own

')] }, { directory: 'dns', children: [javaFile('hosts', null, 'a.de. A 3600 10.0.0.1\n')] }, // Made by the peer-to-peer application and the e-mail server, whose directories the files lack. { directory: 'peer2peer', children: [] }, { directory: 'mailserver', children: [] }, ], }, // In the order of Java's key. applications: [ { key: 'filius.software.clientserver.ClientBaustein', class: 'filius.software.clientserver.ClientBaustein', zielIPAdresse: '10.0.0.9', zielPort: 7777 }, { key: 'filius.software.clientserver.ServerBaustein', class: 'filius.software.clientserver.ServerBaustein', aktiv: true, port: 7777 }, { key: 'filius.software.dateiaustausch.PeerToPeerAnwendung', class: 'filius.software.dateiaustausch.PeerToPeerAnwendung', maxTeilnehmerZahl: 7 }, { key: 'filius.software.dns.DNSServer', class: 'filius.software.dns.DNSServer', aktiv: true, port: 5353, recursiveResolutionEnabled: true }, { key: 'filius.software.email.EmailAnwendung', class: 'filius.software.email.EmailAnwendung', received: [javaMail()], sent: [javaMail({ versendet: true, neu: false })], drafts: [javaMail({ betreff: null, text: null, dateReceived: null, empfaenger: [] })], addressBook: [['Muster', 'Anna', null, 12, 20095, 'Hamburg', 'anna@schule.de', null]], // No /konten.txt. fileAccount: null, }, { key: 'filius.software.email.EmailServer', class: 'filius.software.email.EmailServer', aktiv: true, mailDomain: 'schule.de', beanAccounts: [ { benutzername: 'anna', passwort: 'geheim', nachname: 'Muster', vorname: '', emailAdresse: 'anna@schule.de', pop3server: null, pop3port: null, smtpserver: null, smtpport: null, nachrichten: [javaMail()], }, ], // No /mailserver/konten.txt. fileAccounts: null, }, { key: 'filius.software.firewall.Firewall', class: 'filius.software.firewall.Firewall', activated: false, defaultPolicy: 1, dropICMP: true, filterSYNSegmentsOnly: false, filterUdp: false, rules: [['10.0.0.0', '255.0.0.0', '', '', 22, 6, 1]], }, { key: 'filius.software.lokal.FileExplorer', class: 'filius.software.lokal.FileExplorer' }, { key: 'filius.software.lokal.ImageViewer', class: 'filius.software.lokal.ImageViewer' }, { key: 'filius.software.lokal.Terminal', class: 'filius.software.lokal.Terminal' }, { key: 'filius.software.lokal.TextEditor', class: 'filius.software.lokal.TextEditor' }, { key: 'filius.software.www.WebBrowser', class: 'filius.software.www.WebBrowser' }, { key: 'filius.software.www.WebServer', class: 'filius.software.www.WebServer', aktiv: true, port: 8080, useVHost: true }, ], }, }, { class: 'Switch', name: 'S', position: [100, 20], useIpAsName: undefined, useMacAsName: undefined, ports: [{}, {}, {}], system: { ssid: 'S-net', retentionTime: 1234 }, }, { class: 'Vermittlungsrechner', name: 'R', position: [200, 20], useIpAsName: undefined, useMacAsName: undefined, ports: ['1', '2', '3'].map(plainInterface), system: { ripEnabled: true, fileSystem: { directory: 'root', children: [...routerTemplate(), { directory: 'www.conf', children: [] }] }, manualRoutes: [], dhcpClient: false, dhcpServer: JAVA_DHCP_DEFAULTS, firewall: { activated: true, defaultPolicy: 1, dropICMP: true, filterSYNSegmentsOnly: false, filterUdp: false, rules: [ ['10.0.1.0', '255.255.255.0', '', '', -1, -1, 1], ['', '', '10.0.2.5', '255.255.255.255', 80, 6, 0], [null, '', '', '', 53, 17, 1], ['', '', '', '', -1, 1, 0], ], }, }, }, { class: 'Modem', name: 'M', position: [300, 20], useIpAsName: undefined, useMacAsName: undefined, ports: [{}], system: { modem: { mode: 1, port: 4242, ipAdresse: '192.168.1.5' } }, }, { class: 'Gateway', name: 'G', position: [400, 20], useIpAsName: undefined, useMacAsName: undefined, ports: ['4', '5'].map(plainInterface), system: { fileSystem: { directory: 'own', children: [javaFile('x', 'text', 'y')] }, manualRoutes: [], dhcpClient: false, dhcpServer: JAVA_DHCP_DEFAULTS, staticNAT: [], firewall: JAVA_ROUTER_FIREWALL, }, }, ], cables: [ { ends: [[0, 0], [1, 2]], wireless: false, panel: [0, 1] }, { ends: [[2, 2], [1, 0]], wireless: true, panel: [2, 1] }, ], documentation: [{ type: 1, text: null, bounds: [5, 6, 70, 80], color: null, font: null }], }); }); });