feat(fls): export the editor model's programs
exportProject.ts, step X4a: each installed program of a PC or notebook as the FILIUS application of its class, keyed by that class name as Java keys them, with its settings: servers switched on as the model has them, a web server's port and virtual hosts, the DNS server's recursion on port 53, the echo server's port, the generic client's target (none as null, as its constructor leaves it), the peer count, the personal firewall. E-mail contents are the next step. The model lost a web server's virtual hosts switch (useVHost, on for 3 web servers in the corpus); Swift sets it off on import. The core now has WebServerConfiguration.virtualHosts (Swift's name), the import carries Java's value, and the Swift comparison names Swift's error. Checked by Java: every corpus file, imported, exported and written, loads with the programs of the original, with no new departure. In the synthetic model the PC has every program with settings away from the defaults, the notebook programs without settings (Java's defaults, 3 peers), where Java makes /peer2peer itself. 12 mutation probes, each loaded by Java, were all caught. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,17 @@
|
||||
// Java's root, each file a Datei of the type Java's importer reads it by —
|
||||
// text by its extension (else "text"), an image or binary data by its media
|
||||
// type, in Base64 as FILIUS's Base64.encodeBytes writes it (javaBase64). A router or home
|
||||
// router keeps the files its constructor makes; the model holds none for it. System software, files and programs
|
||||
// router keeps the files its constructor makes; the model holds none for it.
|
||||
//
|
||||
// The programs of a PC or notebook: each installed program as the FILIUS
|
||||
// application of its class, under that class name as Java keys them, with
|
||||
// its settings — a server switched on as the model has it, a web server's
|
||||
// port and virtual hosts, the DNS server's recursion (on port 53, where the
|
||||
// model keeps it), the echo server's port, the generic client's target, the
|
||||
// peer count, the personal firewall. A DNS server's records are in
|
||||
// /dns/hosts, one of the files, and it forwards to the host's DNS server as
|
||||
// Java's does. E-mail accounts, messages and address books are a later
|
||||
// step. System software, files and programs
|
||||
// follow in later steps.
|
||||
//
|
||||
// What the model keeps is written as it keeps it, and so differs from the
|
||||
@@ -40,7 +50,9 @@
|
||||
// derives from the node's id;
|
||||
// - files in the model's order (directories first, then by name) and of the
|
||||
// type their kind says, not the one the file named; a router's own files
|
||||
// (a web server's www.conf) are not in the model.
|
||||
// (a web server's www.conf) are not in the model;
|
||||
// - a peer-to-peer application's peer count the file lacked (Java rolls
|
||||
// 3, 4 or 5) is the model's 3.
|
||||
//
|
||||
// Checked by Java: every corpus file, imported, exported and written, loads in
|
||||
// Java FILIUS to the topology of the original up to those departures.
|
||||
@@ -52,10 +64,17 @@ import {
|
||||
effectiveMacAddress,
|
||||
firewallConfiguration,
|
||||
FIREWALL_PROTOCOL,
|
||||
JAVA_PERSONAL_FIREWALL_DEFAULTS,
|
||||
interfaceKey,
|
||||
type DeviceConfiguration,
|
||||
type DhcpClientConfiguration,
|
||||
type DhcpServerConfiguration,
|
||||
type DnsServerConfiguration,
|
||||
type EchoServerConfiguration,
|
||||
type GnutellaConfiguration,
|
||||
type InstallableProgram,
|
||||
type SimpleClientConfiguration,
|
||||
type WebServerConfiguration,
|
||||
type FirewallConfiguration,
|
||||
type GatewayPortForwardingRow,
|
||||
type ManualRoute,
|
||||
@@ -72,9 +91,10 @@ import {
|
||||
type TopologyNode,
|
||||
type VirtualFileSystem,
|
||||
} from '@mdg-filius/core';
|
||||
import type { FlsDhcpServer, FlsFileTree, FlsFirewall } from './systemSoftware.js';
|
||||
import { PROGRAM_OF_CLASS } from './importProject.js';
|
||||
import type { FlsApplicationClass, FlsDhcpServer, FlsFileTree, FlsFirewall } from './systemSoftware.js';
|
||||
import type { FlsDocumentationItem, FlsInterface, FlsLink, FlsNodeClass } from './topology.js';
|
||||
import type { NodeToWrite, SystemToWrite, TopologyToWrite } from './topologyWriter.js';
|
||||
import type { ApplicationToWrite, NodeToWrite, SystemToWrite, TopologyToWrite } from './topologyWriter.js';
|
||||
|
||||
/** The parts of the editor model the topology is made of. */
|
||||
export interface ProjectToExport {
|
||||
@@ -91,6 +111,13 @@ export interface ProjectToExport {
|
||||
readonly portForwardingRowsByNodeId: ReadonlyMap<Id, readonly GatewayPortForwardingRow[]>;
|
||||
readonly remoteLinkConfigurations: ReadonlyMap<Id, RemoteLinkConfiguration>;
|
||||
readonly virtualFileSystems: ReadonlyMap<Id, VirtualFileSystem>;
|
||||
readonly installedPrograms: ReadonlyMap<Id, ReadonlySet<InstallableProgram>>;
|
||||
readonly activePrograms: ReadonlyMap<Id, ReadonlySet<InstallableProgram>>;
|
||||
readonly webServerConfigurations: ReadonlyMap<Id, WebServerConfiguration>;
|
||||
readonly gnutellaConfigurations: ReadonlyMap<Id, GnutellaConfiguration>;
|
||||
readonly dnsServerConfigurations: ReadonlyMap<Id, DnsServerConfiguration>;
|
||||
readonly echoServerConfigurations: ReadonlyMap<Id, EchoServerConfiguration>;
|
||||
readonly simpleClientConfigurations: ReadonlyMap<Id, SimpleClientConfiguration>;
|
||||
readonly documentationItems: readonly DocumentationItem[];
|
||||
}
|
||||
|
||||
@@ -223,6 +250,7 @@ function exportSystem(node: TopologyNode, project: ProjectToExport, servingModem
|
||||
...system,
|
||||
ssid: ssid === '' ? null : ssid,
|
||||
fileSystem: { kind: 'directory', name: 'root', children: files === undefined ? [] : fileTrees(files, '/') },
|
||||
applications: exportPrograms(id, project),
|
||||
};
|
||||
}
|
||||
case 'router':
|
||||
@@ -245,6 +273,51 @@ function exportSystem(node: TopologyNode, project: ProjectToExport, servingModem
|
||||
}
|
||||
}
|
||||
|
||||
const CLASS_OF_PROGRAM = Object.fromEntries(
|
||||
Object.entries(PROGRAM_OF_CLASS).map(([javaClass, program]) => [program, javaClass]),
|
||||
) as Readonly<Record<InstallableProgram, FlsApplicationClass>>;
|
||||
|
||||
const DNS_PORT = 53;
|
||||
const ECHO_PORT = 55_555;
|
||||
|
||||
function exportPrograms(id: Id, project: ProjectToExport): ApplicationToWrite[] {
|
||||
const active = project.activePrograms.get(id);
|
||||
return [...(project.installedPrograms.get(id) ?? [])].map((program): ApplicationToWrite => {
|
||||
const javaClass = CLASS_OF_PROGRAM[program];
|
||||
const application = { key: javaClass, javaClass };
|
||||
const isActive = active?.has(program) === true;
|
||||
switch (program) {
|
||||
case 'webServer': {
|
||||
const web = project.webServerConfigurations.get(id);
|
||||
return { ...application, isActive, port: web?.port ?? 80, useVirtualHosts: web?.virtualHosts === true };
|
||||
}
|
||||
case 'dnsServer':
|
||||
return {
|
||||
...application,
|
||||
isActive,
|
||||
port: DNS_PORT,
|
||||
recursiveResolutionEnabled: project.dnsServerConfigurations.get(id)?.recursiveResolutionEnabled === true,
|
||||
};
|
||||
case 'echoServer':
|
||||
return { ...application, isActive, port: project.echoServerConfigurations.get(id)?.port ?? ECHO_PORT };
|
||||
case 'simpleClient': {
|
||||
const client = project.simpleClientConfigurations.get(id);
|
||||
// No target is null, as ClientBaustein's constructor leaves it.
|
||||
const target = client?.targetAddress ?? '';
|
||||
return { ...application, targetAddress: target === '' ? null : target, targetPort: client?.targetPort ?? ECHO_PORT };
|
||||
}
|
||||
case 'gnutella':
|
||||
return { ...application, maxPeers: project.gnutellaConfigurations.get(id)?.maximumKnownPeers ?? 3 };
|
||||
case 'firewall':
|
||||
return { ...application, firewall: exportFirewall(project.firewallConfigurationsByNodeId.get(id) ?? JAVA_PERSONAL_FIREWALL_DEFAULTS) };
|
||||
case 'emailServer':
|
||||
return { ...application, isActive };
|
||||
default:
|
||||
return application;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** The text types Java's importer reads as text, named by a file's extension. */
|
||||
const TEXT_EXTENSIONS = new Set(['txt', 'html', 'htm', 'css', 'xml', 'csv', 'conf', 'log']);
|
||||
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
// prompt, text editor, file explorer, image viewer, echo server and
|
||||
// generic client, and their settings (port, target) have a place here;
|
||||
// - a web server's files stay in /webserver (its document root), where Swift
|
||||
// moves them to /www; and no program state is written into a host's files
|
||||
// moves them to /www, and it keeps Java's virtual hosts switch (Swift sets
|
||||
// it off); and no program state is written into a host's files
|
||||
// (Swift keeps Gnutella and e-mail state there as hidden JSON files).
|
||||
// - e-mail: see importEmail.ts;
|
||||
// - a DNS server's records are what Java's server answers from /dns/hosts
|
||||
@@ -422,6 +423,7 @@ function registerPrograms(source: FlsNode, node: TopologyNode, system: SystemCon
|
||||
system.webServerConfigurations.set(node.id, {
|
||||
port: application.port ?? DEFAULT_WEB_SERVER_PORT,
|
||||
documentRoot: JAVA_WEB_ROOT,
|
||||
virtualHosts: application.useVirtualHosts === true,
|
||||
});
|
||||
break;
|
||||
case 'gnutella':
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
firewallRule,
|
||||
stableGeneratedMacAddress,
|
||||
type Id,
|
||||
type InstallableProgram,
|
||||
type TopologyNode,
|
||||
VirtualFileSystem,
|
||||
} from '@mdg-filius/core';
|
||||
@@ -68,9 +69,21 @@ const original = JSON.parse(readFileSync(join(corpusRoot, 'corpus.java.json'), '
|
||||
const exportedPath = join(here, 'exported.java.json');
|
||||
const exported = JSON.parse(readFileSync(exportedPath, 'utf8')) as Record<string, JavaFile>;
|
||||
|
||||
/** The system software without its applications, a later step; files in any order. */
|
||||
/**
|
||||
* The system software without what a later step exports — the e-mail
|
||||
* programs' domain, bean accounts, folders and address book; files in any
|
||||
* order.
|
||||
*/
|
||||
function systemOf(system: Record<string, unknown>) {
|
||||
const { applications: _applications, ...settings } = system;
|
||||
const settings =
|
||||
system.applications === undefined
|
||||
? system
|
||||
: {
|
||||
...system,
|
||||
applications: (system.applications as Record<string, unknown>[]).map(
|
||||
({ mailDomain: _domain, beanAccounts: _accounts, received: _received, sent: _sent, drafts: _drafts, addressBook: _book, ...rest }) => rest,
|
||||
),
|
||||
};
|
||||
return settings.fileSystem === undefined || settings.fileSystem === null
|
||||
? settings
|
||||
: { ...settings, fileSystem: sortedTree(settings.fileSystem as JavaTree) };
|
||||
@@ -315,6 +328,13 @@ function emptyProject(): ProjectToExport {
|
||||
portForwardingRowsByNodeId: new Map(),
|
||||
remoteLinkConfigurations: new Map(),
|
||||
virtualFileSystems: new Map(),
|
||||
installedPrograms: new Map(),
|
||||
activePrograms: new Map(),
|
||||
webServerConfigurations: new Map(),
|
||||
gnutellaConfigurations: new Map(),
|
||||
dnsServerConfigurations: new Map(),
|
||||
echoServerConfigurations: new Map(),
|
||||
simpleClientConfigurations: new Map(),
|
||||
documentationItems: [],
|
||||
};
|
||||
}
|
||||
@@ -341,6 +361,10 @@ function syntheticFiles(): VirtualFileSystem {
|
||||
files.writeImageFile('/foto.jpg', bytesOf(100), 'image/jpeg');
|
||||
files.writeBinaryFile('/daten.bin', bytesOf(58), 'application/zip');
|
||||
files.writeBinaryFile('/roh', bytesOf(3));
|
||||
// The web server's and the peer-to-peer application's own: Java makes none.
|
||||
files.createDirectory('/webserver');
|
||||
files.writeTextFile('/webserver/index.html', '<p>own</p>');
|
||||
files.createDirectory('/peer2peer');
|
||||
return files;
|
||||
}
|
||||
|
||||
@@ -400,6 +424,14 @@ const SYNTHETIC: ProjectToExport = {
|
||||
]),
|
||||
ripEnabledByNodeId: new Map([['rt', true]]),
|
||||
firewallConfigurationsByNodeId: new Map([
|
||||
[
|
||||
'pc',
|
||||
firewallConfiguration({
|
||||
isActive: false,
|
||||
defaultPolicy: 'accept',
|
||||
rules: [firewallRule({ sourceIPAddress: '10.0.0.0', sourceSubnetMask: '255.0.0.0', port: 22, action: 'accept' })],
|
||||
}),
|
||||
],
|
||||
[
|
||||
'rt',
|
||||
firewallConfiguration({
|
||||
@@ -437,6 +469,34 @@ const SYNTHETIC: ProjectToExport = {
|
||||
]),
|
||||
portForwardingRowsByNodeId: new Map([['gw', [{ protocolValue: 'TCP', publicPortValue: '8080', lanIPAddress: '192.168.1.10', lanPortValue: '80' }]]]),
|
||||
virtualFileSystems: new Map([['pc', syntheticFiles()]]),
|
||||
installedPrograms: new Map<Id, Set<InstallableProgram>>([
|
||||
[
|
||||
'pc',
|
||||
new Set<InstallableProgram>([
|
||||
'webServer',
|
||||
'dnsServer',
|
||||
'echoServer',
|
||||
'simpleClient',
|
||||
'gnutella',
|
||||
'firewall',
|
||||
'commandPrompt',
|
||||
'textEditor',
|
||||
'fileExplorer',
|
||||
'imageViewer',
|
||||
'webBrowser',
|
||||
]),
|
||||
],
|
||||
['nb', new Set<InstallableProgram>(['gnutella', 'simpleClient', 'firewall'])],
|
||||
]),
|
||||
activePrograms: new Map([['pc', new Set<InstallableProgram>(['webServer', 'dnsServer', 'echoServer'])]]),
|
||||
webServerConfigurations: new Map([['pc', { port: 8080, documentRoot: '/webserver', virtualHosts: true }]]),
|
||||
dnsServerConfigurations: new Map([['pc', { recursiveResolutionEnabled: true, records: [] }]]),
|
||||
echoServerConfigurations: new Map([['pc', { port: 7777 }]]),
|
||||
simpleClientConfigurations: new Map([
|
||||
['pc', { targetAddress: '10.0.0.9', targetPort: 7777 }],
|
||||
['nb', { targetAddress: '', targetPort: 55_555 }],
|
||||
]),
|
||||
gnutellaConfigurations: new Map([['pc', { maximumKnownPeers: 7 }]]),
|
||||
remoteLinkConfigurations: new Map([
|
||||
['m1', { pairIdentifier: 'java-modem-port-4242', latencyMilliseconds: 20, isEnabled: true }],
|
||||
['m2', { pairIdentifier: 'java-modem-port-4242', latencyMilliseconds: 20, isEnabled: true }],
|
||||
@@ -520,8 +580,33 @@ describe('a model beyond the corpus, as Java loads it', () => {
|
||||
javaFile('foto.jpg', 'jpg', javaBase64(bytesOf(100))),
|
||||
javaFile('index.HTML', 'html', '<p>upper case extension</p>'),
|
||||
javaFile('roh', '', javaBase64(bytesOf(3))),
|
||||
{ directory: 'webserver', children: [javaFile('index.html', 'html', '<p>own</p>')] },
|
||||
{ directory: 'peer2peer', 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: 53, recursiveResolutionEnabled: true },
|
||||
{
|
||||
key: 'filius.software.firewall.Firewall',
|
||||
class: 'filius.software.firewall.Firewall',
|
||||
activated: false,
|
||||
defaultPolicy: 1,
|
||||
dropICMP: false,
|
||||
filterSYNSegmentsOnly: true,
|
||||
filterUdp: true,
|
||||
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 },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -540,7 +625,29 @@ describe('a model beyond the corpus, as Java loads it', () => {
|
||||
useIpAsName: false,
|
||||
useMacAsName: true,
|
||||
ports: [card('10.0.0.2', '255.255.255.0', stableGeneratedMacAddress('n0'), '', '', true)],
|
||||
system: { ssid: 'net', manualRoutes: [], dhcpClient: false, dhcpServer: JAVA_DHCP_DEFAULTS, fileSystem: { directory: 'root', children: [] } },
|
||||
system: {
|
||||
ssid: 'net',
|
||||
manualRoutes: [],
|
||||
dhcpClient: false,
|
||||
dhcpServer: JAVA_DHCP_DEFAULTS,
|
||||
// Made by the peer-to-peer application.
|
||||
fileSystem: { directory: 'root', children: [{ directory: 'peer2peer', children: [] }] },
|
||||
applications: [
|
||||
// Without a setting: no target (null), the model's 3 peers, Java's personal firewall.
|
||||
{ key: 'filius.software.clientserver.ClientBaustein', class: 'filius.software.clientserver.ClientBaustein', zielIPAdresse: null, zielPort: 55_555 },
|
||||
{ key: 'filius.software.dateiaustausch.PeerToPeerAnwendung', class: 'filius.software.dateiaustausch.PeerToPeerAnwendung', maxTeilnehmerZahl: 3 },
|
||||
{
|
||||
key: 'filius.software.firewall.Firewall',
|
||||
class: 'filius.software.firewall.Firewall',
|
||||
activated: true,
|
||||
defaultPolicy: 0,
|
||||
dropICMP: false,
|
||||
filterSYNSegmentsOnly: true,
|
||||
filterUdp: true,
|
||||
rules: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
class: 'Vermittlungsrechner',
|
||||
|
||||
+1723
-119
File diff suppressed because it is too large
Load Diff
@@ -140,7 +140,7 @@ function systemOf(project: ImportedProject, nodeId: Id) {
|
||||
const web = project.webServerConfigurations.get(nodeId);
|
||||
if (web !== undefined) {
|
||||
// Swift serves the same files from /www after moving them there.
|
||||
result.webServer = { port: web.port, documentRoot: web.documentRoot === '/webserver' ? '/www' : web.documentRoot, virtualHosts: false };
|
||||
result.webServer = { port: web.port, documentRoot: web.documentRoot === '/webserver' ? '/www' : web.documentRoot, virtualHosts: web.virtualHosts };
|
||||
}
|
||||
const gnutella = project.gnutellaConfigurations.get(nodeId);
|
||||
if (gnutella !== undefined) result.gnutella = gnutella;
|
||||
@@ -330,7 +330,15 @@ const unnumberedMailbox = (numbered: string[]) => (node: SwiftNode) => {
|
||||
},
|
||||
};
|
||||
};
|
||||
/** Swift reads a web server's useVHost as off. */
|
||||
const servesVirtualHosts = (node: SwiftNode) => ({
|
||||
...node,
|
||||
system: { ...node.system, webServer: { ...(node.system.webServer as object), virtualHosts: true } },
|
||||
});
|
||||
const SWIFT_ERRORS: Record<string, Record<number, (node: SwiftNode) => SwiftNode>> = {
|
||||
'ipad-fixtures/Public_and_Private_Networks_2_EN.fls': { 2: servesVirtualHosts },
|
||||
'java-examples/Public_and_Private_Networks_2_EN.fls': { 2: servesVirtualHosts },
|
||||
'java-examples/private_und_public_networks_mit_provider_DE.fls': { 6: servesVirtualHosts },
|
||||
'java-examples/man-in-the-middle.fls': { 5: unnumberedMailbox(['1', '2']) },
|
||||
'java-examples/multicast_server.fls': {
|
||||
// useIPAsName is `<object idref="Boolean0"/>`, the shared result of a
|
||||
@@ -894,7 +902,7 @@ describe('importProject rules', () => {
|
||||
|
||||
expect([...project.installedPrograms.get(notebook!)!].sort()).toEqual(['commandPrompt', 'echoServer', 'firewall', 'simpleClient', 'webServer']);
|
||||
expect([...project.activePrograms.get(notebook!)!]).toEqual(['webServer']);
|
||||
expect(project.webServerConfigurations.get(notebook!)).toEqual({ port: 8080, documentRoot: '/webserver' });
|
||||
expect(project.webServerConfigurations.get(notebook!)).toEqual({ port: 8080, documentRoot: '/webserver', virtualHosts: false });
|
||||
expect(project.echoServerConfigurations.get(notebook!)).toEqual({ port: 7 });
|
||||
expect(project.simpleClientConfigurations.get(notebook!)).toEqual({ targetAddress: '10.0.0.9', targetPort: 7 });
|
||||
expect(project.firewallConfigurationsByNodeId.get(notebook!)).toMatchObject({ isActive: true, defaultPolicy: 'accept' });
|
||||
|
||||
Reference in New Issue
Block a user