Files
mdg-filius/packages/fls/src/exportProject.ts
T
julian.kiedaischandClaude Opus 5.5 8ab8850b20 feat(fls): export the editor model's e-mail
exportProject.ts, step X4b, which completes the export: the e-mail beans
from the model — the server's domain, accounts and mailboxes, the client's
three folders and address book. The account files are written anew from
the model's accounts, so that they say what the model says:
/mailserver/konten.txt, which Java's server reads at every simulation
start, and /konten.txt, the client's account. The format is FILIUS 2.14's
(listeBenutzerkontenZuString, EmailKonto.toString) without its errors:
empty fields where Java writes "null". Java's readers drop empty fields
(StringTokenizer) and a trailing empty one (String.split); the files are
written as a correct reader, this package's, reads them. Java escapes only
a message's text; a character another field cannot carry (a separator, a
line break) becomes a space there, with a warning, and the beans keep it.

Checked by Java: every corpus file, imported, exported and written, loads
with the e-mail programs of the original up to named departures: a string
recipient read as an address (as the writer does), the sent folder's
messages sent (the import's decision), the account files written anew
with Java's types and without the line break older versions ended the
client's with. In the synthetic model Java reads both account files back
as the model means them, every escaped character of the text included.
17 mutation probes, each loaded by Java, were all caught.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:25:22 +00:00

640 lines
27 KiB
TypeScript

// From the editor model to Java's view of a .fls file, which
// topologyWriter.ts writes — the counterpart of importProject.ts.
//
// The topology: each node becomes the Java device of its kind, with its name,
// position and label policy; its ports become interfaces (PC, notebook,
// router, home router) or ports (switch, modem) of Java's device; each link
// becomes a cable between those, drawn between its two nodes; each
// documentation item a GUIDocuItem. A host whose wireless is enabled has a
// wireless card, and its link to a switch is a wireless cable — the only
// wireless connection Java makes (JHostKonfiguration.updateWifiConnection).
//
// The system software: a switch's SSID and learning time, a host's SSID,
// manual routes, RIP, DHCP client and server, a home router's port
// forwarding, the firewall of a router or home router, and a modem. What the
// model has no setting for is what Java's constructors make. A remote link
// pair becomes modems on the port its identifier names (12345 when it names
// none): the first of a pair in node order serves, the others connect to
// localhost — the model's pairs live in one project.
//
// The files of a PC or notebook: the model's directories and files under
// 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.
//
// 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: the server's domain and accounts with their mailboxes, the client's
// folders and address book, as Java's beans. The account files are written
// anew from the model's accounts, in the format Java writes them
// (EmailServer.listeBenutzerkontenZuString, EmailKonto.toString), so that
// they say what the model says: /mailserver/konten.txt, which Java's server
// reads at every simulation start, and /konten.txt, the client's account.
// Java's errors are not copied: a field is written empty where Java writes
// "null", and the format is written as Java's own readers should read it —
// they drop empty fields (StringTokenizer) and a trailing empty one
// (String.split), the readers of this package do not. A character a field
// cannot carry in the format (a separator, a line break) becomes a space,
// with a warning; the beans keep it. System software, files and programs
// follow in later steps.
//
// What the model keeps is written as it keeps it, and so differs from the
// Java file it may have been imported from where the import decides (named
// in exportProject.test.ts):
// - a port's MAC address is the one the simulation uses: the file's, or
// one derived from the port's id (effectiveMacAddress) — stable across
// saves, where Java rolls a new one at each load;
// - a router's or home router's gateway and DNS server are written to every
// interface, as Java's own setters (setStandardGateway, setDNSServer) do:
// Java reads them from the first interface only (getStandardGateway,
// getDNSServer), and the model keeps no others;
// - a documentation item always has a text and a font, bold or plain: where
// Java has none, the empty text and Dialog 12 the model gives it;
// - a switch's SSID the file lacked (Java rolls one) is the one the model
// 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 peer-to-peer application's peer count the file lacked (Java rolls
// 3, 4 or 5) is the model's 3;
// - e-mail as the model keeps it (importEmail.ts): an older file's client
// account in /konten.txt, where FILIUS 2.14 no longer reads it; account
// files written from the model's accounts.
//
// Checked by Java: every corpus file, imported, exported and written, loads in
// Java FILIUS to the topology of the original up to those departures.
import {
base64Encode,
defaultSwitchConfiguration,
dhcpServerConfiguration,
effectiveMacAddress,
firewallConfiguration,
FIREWALL_PROTOCOL,
JAVA_PERSONAL_FIREWALL_DEFAULTS,
interfaceKey,
type DeviceConfiguration,
type DhcpClientConfiguration,
type DhcpServerConfiguration,
type DnsServerConfiguration,
type EchoServerConfiguration,
type EmailAddress,
type EmailClientConfiguration,
type EmailContact,
type EmailMessage,
type EmailServerConfiguration,
type GnutellaConfiguration,
type InstallableProgram,
type SimpleClientConfiguration,
type WebServerConfiguration,
type FirewallConfiguration,
type GatewayPortForwardingRow,
type ManualRoute,
type RemoteLinkConfiguration,
type SwitchConfiguration,
type DocumentationItem,
type HostLabelPolicy,
type HostWirelessConfiguration,
type Id,
type InterfaceConfiguration,
type InterfaceKey,
type NodeKind,
type TopologyGraph,
type TopologyNode,
type VirtualFileSystem,
} from '@mdg-filius/core';
import { EMAIL_CLIENT_FILE, EMAIL_SERVER_FILE, type JavaAddressEntry, type JavaContact, type JavaEmail } from './javaEmail.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 { ApplicationToWrite, NodeToWrite, SystemToWrite, TopologyToWrite } from './topologyWriter.js';
/** The parts of the editor model the topology is made of. */
export interface ProjectToExport {
readonly graph: TopologyGraph;
readonly deviceConfigurations: ReadonlyMap<Id, DeviceConfiguration>;
readonly interfaceConfigurations: ReadonlyMap<InterfaceKey, InterfaceConfiguration>;
readonly hostWirelessConfigurations: ReadonlyMap<Id, HostWirelessConfiguration>;
readonly switchConfigurations: ReadonlyMap<Id, SwitchConfiguration>;
readonly manualRoutesByNodeId: ReadonlyMap<Id, readonly ManualRoute[]>;
readonly ripEnabledByNodeId: ReadonlyMap<Id, boolean>;
readonly firewallConfigurationsByNodeId: ReadonlyMap<Id, FirewallConfiguration>;
readonly dhcpClientConfigurations: ReadonlyMap<Id, DhcpClientConfiguration>;
readonly dhcpServerConfigurations: ReadonlyMap<Id, DhcpServerConfiguration>;
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 emailClientConfigurations: ReadonlyMap<Id, EmailClientConfiguration>;
readonly emailServerConfigurations: ReadonlyMap<Id, EmailServerConfiguration>;
readonly documentationItems: readonly DocumentationItem[];
}
export interface ExportedTopology {
readonly topology: TopologyToWrite;
readonly warnings: string[];
}
const CLASS_OF_KIND: Readonly<Record<Exclude<NodeKind, 'unsupported'>, FlsNodeClass>> = {
pc: 'Rechner',
notebook: 'Notebook',
networkSwitch: 'Switch',
router: 'Vermittlungsrechner',
gateway: 'Gateway',
remoteLink: 'Modem',
};
export function exportProject(project: ProjectToExport): ExportedTopology {
const warnings: string[] = [];
const exported = project.graph.nodes.filter((node) => {
if (node.kind !== 'unsupported') return true;
warnings.push(`node '${node.displayName}': a kind Java FILIUS does not have, left out`);
return false;
});
const indexOfNode = new Map(exported.map((node, index) => [node.id, index]));
const servingModems = firstOfEachPair(exported, project);
const nodes = exported.map((node) => exportNode(node, project, servingModems, warnings));
const links: FlsLink[] = [];
project.graph.links.forEach((link, index) => {
const source = indexOfNode.get(link.sourceNodeId);
const target = indexOfNode.get(link.targetNodeId);
const sourcePort = source === undefined ? -1 : exported[source]!.ports.findIndex((port) => port.id === link.sourcePortId);
const targetPort = target === undefined ? -1 : exported[target]!.ports.findIndex((port) => port.id === link.targetPortId);
if (source === undefined || target === undefined || sourcePort < 0 || targetPort < 0) {
warnings.push(`link ${index + 1}: an end is not a port of an exported node, left out`);
return;
}
links.push({
ends: [
{ node: source, port: sourcePort },
{ node: target, port: targetPort },
],
wireless: joinsWirelessly(exported[source]!, exported[target]!, project),
panel: [source, target],
});
});
return {
topology: { nodes, links, documentation: project.documentationItems.map(exportDocumentationItem) },
warnings,
};
}
function exportNode(node: TopologyNode, project: ProjectToExport, servingModems: ReadonlySet<Id>, warnings: string[]): NodeToWrite {
const javaClass = CLASS_OF_KIND[node.kind as Exclude<NodeKind, 'unsupported'>];
const interfaces = exportInterfaces(node, project);
return {
javaClass,
name: node.displayName,
x: Math.round(node.position.x),
y: Math.round(node.position.y),
useIpAsName: showsIp(node.hostLabelPolicy),
useMacAsName: showsMac(node.hostLabelPolicy),
interfaces,
portCount: javaClass === 'Switch' || javaClass === 'Modem' ? node.ports.length : interfaces.length,
system: exportSystem(node, project, servingModems, (message) => warnings.push(`node '${node.displayName}': ${message}`)),
};
}
/** The first node of each remote link pair: its modem serves. */
function firstOfEachPair(nodes: readonly TopologyNode[], project: ProjectToExport): Set<Id> {
const seen = new Set<string>();
const serving = new Set<Id>();
for (const node of nodes) {
if (node.kind !== 'remoteLink') continue;
const pair = project.remoteLinkConfigurations.get(node.id)?.pairIdentifier ?? '';
if (seen.has(pair)) continue;
seen.add(pair);
serving.add(node.id);
}
return serving;
}
const JAVA_MODEM_PAIR = /^java-modem-port-([0-9]{1,5})$/;
const DEFAULT_MODEM_PORT = 12_345;
function modemPort(pairIdentifier: string | undefined): number {
const match = JAVA_MODEM_PAIR.exec(pairIdentifier ?? '');
const port = match === null ? DEFAULT_MODEM_PORT : Number(match[1]);
return port >= 1 && port <= 65_535 ? port : DEFAULT_MODEM_PORT;
}
type Warn = (message: string) => void;
function exportSystem(node: TopologyNode, project: ProjectToExport, servingModems: ReadonlySet<Id>, warn: Warn): SystemToWrite {
const id = node.id;
switch (node.kind) {
case 'networkSwitch': {
const configuration = project.switchConfigurations.get(id) ?? defaultSwitchConfiguration(id);
return { ssid: configuration.ssid, retentionTime: configuration.retentionTimeMilliseconds };
}
case 'remoteLink':
return {
modem: {
mode: servingModems.has(id) ? 1 : 2,
port: modemPort(project.remoteLinkConfigurations.get(id)?.pairIdentifier),
ipAddress: 'localhost',
},
};
case 'unsupported':
return {};
default:
break;
}
const system: SystemToWrite = {
manualRoutes: (project.manualRoutesByNodeId.get(id) ?? []).map((route) => [
route.destinationNetwork,
route.subnetMask,
route.gateway,
route.interfaceIPAddress,
]),
dhcpClient: project.dhcpClientConfigurations.get(id)?.isEnabled === true,
dhcpServer: exportDhcpServer(project.dhcpServerConfigurations.get(id) ?? dhcpServerConfiguration()),
};
switch (node.kind) {
case 'pc':
case 'notebook': {
const ssid = project.hostWirelessConfigurations.get(id)?.ssid ?? '';
const files = project.virtualFileSystems.get(id);
const root: FlsFileTree = { kind: 'directory', name: 'root', children: files === undefined ? [] : fileTrees(files, '/') };
const client = project.installedPrograms.get(id)?.has('emailClient') === true ? project.emailClientConfigurations.get(id) : undefined;
const server = project.installedPrograms.get(id)?.has('emailServer') === true ? project.emailServerConfigurations.get(id) : undefined;
if (client?.account != null) putFile(root, EMAIL_CLIENT_FILE, 'text/txt', clientAccountFile(client, warn));
if (server !== undefined) putFile(root, EMAIL_SERVER_FILE, 'txt', serverAccountsFile(server, warn));
return { ...system, ssid: ssid === '' ? null : ssid, fileSystem: root, applications: exportPrograms(id, project) };
}
case 'router':
return {
...system,
ripEnabled: project.ripEnabledByNodeId.get(id) === true,
firewall: exportFirewall(project.firewallConfigurationsByNodeId.get(id) ?? firewallConfiguration()),
};
case 'gateway':
return {
...system,
staticNAT: (project.portForwardingRowsByNodeId.get(id) ?? []).map((row) => [
row.protocolValue,
row.publicPortValue,
row.lanIPAddress,
row.lanPortValue,
]),
firewall: exportFirewall(project.firewallConfigurationsByNodeId.get(id) ?? firewallConfiguration()),
};
}
}
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': {
const server = project.emailServerConfigurations.get(id);
return {
...application,
isActive,
mailDomain: server?.domain ?? 'filius.de',
beanAccounts: (server?.accounts ?? []).map((account) => ({
username: account.username,
password: account.password,
lastName: account.lastName,
firstName: account.firstName,
emailAddress: null,
pop3Server: null,
pop3Port: null,
smtpServer: null,
smtpPort: null,
messages: account.mailbox.map(javaEmail),
})),
};
}
case 'emailClient': {
const client = project.emailClientConfigurations.get(id);
return {
...application,
received: (client?.inbox ?? []).map(javaEmail),
sent: (client?.sent ?? []).map(javaEmail),
drafts: (client?.drafts ?? []).map(javaEmail),
addressBook: (client?.addressBook ?? []).map(javaContact),
};
}
default:
return application;
}
});
}
function javaAddress(address: EmailAddress): JavaAddressEntry {
return { name: address.name, mailAddress: address.mailAddress };
}
/** A message as Java's Email; its received time as the digits Java keeps, none as Java's "". */
function javaEmail(message: EmailMessage): JavaEmail {
return {
sender: javaAddress(message.from),
to: message.to.map(javaAddress),
cc: message.cc.map(javaAddress),
bcc: message.bcc.map(javaAddress),
subject: message.subject,
text: message.body,
dateReceived: message.receivedAtMilliseconds === undefined ? '' : String(message.receivedAtMilliseconds),
isNew: message.isNew,
isDeleted: message.isMarkedForDeletion,
isSent: message.isSent,
};
}
function javaContact(contact: EmailContact): JavaContact {
return { ...contact };
}
/** A field of an account file: each character it cannot carry becomes a space, with a warning. */
function field(value: string, forbidden: RegExp, what: string, warn: Warn): string {
if (!forbidden.test(value)) return value;
warn(`${what} holds a character Java's account file cannot carry, written there with a space`);
return value.replace(new RegExp(forbidden.source, 'g'), ' ');
}
/** EmailKonto.toString: nine fields; a field may hold no ';' and no line break. */
function clientAccountFile(client: EmailClientConfiguration, warn: Warn): string {
const account = client.account!;
const text = (value: string, what: string) => field(value, /[;\r\n]/, `${EMAIL_CLIENT_FILE}: ${what}`, warn);
return [
text(account.pop3Host, 'POP3 server'),
text(account.smtpHost, 'SMTP server'),
String(account.pop3Port),
String(account.smtpPort),
text(account.username, 'user name'),
text(account.password, 'password'),
text(account.lastName, 'last name'),
text(account.firstName, 'first name'),
text(account.email, 'address'),
].join(';');
}
/**
* EmailServer.listeBenutzerkontenZuString: per account a line of user,
* domain, password, last and first name, then its messages, each "#" and
* seven fields joined by "$" — the text with Java's escapes
* (replaceSpecialChar, then line breaks as "|"), the others unescaped.
*/
function serverAccountsFile(server: EmailServerConfiguration, warn: Warn): string {
const where = (what: string) => `${EMAIL_SERVER_FILE}: ${what}`;
const plain = (value: string, what: string) => field(value, /[;#$\r\n]/, where(what), warn);
const address = (entry: EmailAddress, what: string) => {
const name = entry.name === null ? null : field(entry.name, /[;#$,<>\r\n]/, where(what), warn);
const mail = entry.mailAddress === null ? null : field(entry.mailAddress, /[;#$,<>\r\n]/, where(what), warn);
if (name === null) return mail === null ? '' : `<${mail}>`;
return mail === null ? name : `${name} <${mail}>`;
};
const addresses = (entries: readonly EmailAddress[], what: string) => entries.map((entry) => address(entry, what)).join(',');
const escapedText = (text: string) =>
text.replace(/\$/g, '&&00036&&').replace(/;/g, '&&00059&&').replace(/\|/g, '&&00124&&').replace(/#/g, '&&00035&&').replace(/\n/g, '|');
return server.accounts
.map((account) => {
const messages = account.mailbox
.map(
(message) =>
'#' +
[
address(message.from, 'a sender'),
addresses(message.to, 'a recipient'),
addresses(message.cc, 'a recipient'),
addresses(message.bcc, 'a recipient'),
message.receivedAtMilliseconds === undefined ? '' : String(message.receivedAtMilliseconds),
plain(message.subject, 'a subject'),
field(escapedText(message.body), /\r$/, where('a text'), warn),
].join('$'),
)
.join('');
return [
plain(account.username, 'a user name'),
server.domain,
plain(account.password, 'a password'),
plain(account.lastName, 'a last name'),
plain(account.firstName, 'a first name'),
messages,
].join(';') + '\n';
})
.join('');
}
/**
* The file at an absolute path of the root's tree, replacing one of the same
* name in any case (Java's holeDatei finds it so); directories are made.
*/
function putFile(root: FlsFileTree, path: string, type: string, content: string): void {
const names = path.split('/').filter((name) => name !== '');
let directory = root;
for (const name of names.slice(0, -1)) {
let next = directory.children.find((child) => child.kind === 'directory' && child.name?.toLowerCase() === name);
if (next === undefined) {
next = { kind: 'directory', name, children: [] };
directory.children.push(next);
}
directory = next;
}
const name = names.at(-1)!;
const file: FlsFileTree = { kind: 'file', name, type, content, children: [] };
const index = directory.children.findIndex((child) => child.kind === 'file' && child.name?.toLowerCase() === name);
if (index < 0) directory.children.push(file);
else directory.children[index] = file;
}
/** 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']);
function fileTrees(files: VirtualFileSystem, directory: string): FlsFileTree[] {
return files.entries(directory).map((entry): FlsFileTree => {
const name = entry.path.slice(entry.path.lastIndexOf('/') + 1);
const file = (type: string | null, content: string): FlsFileTree => ({ kind: 'file', name, type, content, children: [] });
switch (entry.content.kind) {
case 'directory':
return { kind: 'directory', name, children: fileTrees(files, entry.path) };
case 'text': {
const dot = name.lastIndexOf('.');
const extension = dot < 0 ? '' : name.slice(dot + 1).toLowerCase();
return file(TEXT_EXTENSIONS.has(extension) ? extension : 'text', entry.content.text);
}
case 'image':
return file(subtype(entry.content.mediaType), javaBase64(entry.content.data));
case 'binary':
return file(entry.content.mediaType === undefined ? '' : subtype(entry.content.mediaType), javaBase64(entry.content.data));
}
});
}
/**
* FILIUS's Base64.encodeBytes (iHarder Base64 2.x): a line break after every
* 76 characters — also after the last full line — then the padded rest.
*/
export function javaBase64(bytes: Uint8Array): string {
const full = bytes.length - (bytes.length % 3);
let text = '';
for (let offset = 0; offset < full; offset += 57) {
const end = Math.min(offset + 57, full);
text += base64Encode(bytes.subarray(offset, end));
if (end - offset === 57) text += '\n';
}
return text + base64Encode(bytes.subarray(full));
}
/** "image/png" → "png", "image/jpeg" → "jpg", as Java names them. */
function subtype(mediaType: string): string {
const name = mediaType.slice(mediaType.indexOf('/') + 1);
return name === 'jpeg' ? 'jpg' : name;
}
function exportDhcpServer(server: DhcpServerConfiguration): FlsDhcpServer {
return {
isActive: server.isActive,
lowerBound: server.lowerBoundIPAddress,
upperBound: server.upperBoundIPAddress,
gateway: server.gatewayIPAddress,
dnsServer: server.dnsServerIPAddress,
useOwnSettings: server.useOwnSettings,
staticAssignedAddresses: server.staticAssignments.map((entry) => ({ mac: entry.macAddress, ip: entry.ipAddress })),
};
}
/** The core's names as Java's numbers: FirewallRule.DROP = 0, ACCEPT = 1. */
function exportFirewall(firewall: FirewallConfiguration): FlsFirewall {
const action = (value: 'drop' | 'accept') => (value === 'accept' ? 1 : 0);
return {
activated: firewall.isActive,
defaultPolicy: action(firewall.defaultPolicy),
dropICMP: firewall.dropIcmp,
filterSYNSegmentsOnly: firewall.filterSynSegmentsOnly,
filterUdp: firewall.filterUdp,
rules: firewall.rules.map((rule) => ({
srcIP: rule.sourceIPAddress,
srcMask: rule.sourceSubnetMask,
destIP: rule.destinationIPAddress,
destMask: rule.destinationSubnetMask,
port: rule.port,
protocol: FIREWALL_PROTOCOL[rule.protocol],
action: action(rule.action),
})),
};
}
function wirelessCard(node: TopologyNode, project: ProjectToExport): boolean {
return (node.kind === 'pc' || node.kind === 'notebook') && project.hostWirelessConfigurations.get(node.id)?.isEnabled === true;
}
/** A host with a wireless card and a switch. */
function joinsWirelessly(first: TopologyNode, second: TopologyNode, project: ProjectToExport): boolean {
return (
(wirelessCard(first, project) && second.kind === 'networkSwitch') ||
(wirelessCard(second, project) && first.kind === 'networkSwitch')
);
}
function showsIp(policy: HostLabelPolicy): boolean {
return policy === 'ipAddress' || policy === 'ipAndMAC';
}
function showsMac(policy: HostLabelPolicy): boolean {
return policy === 'macAddress' || policy === 'ipAndMAC';
}
/**
* The interfaces of a PC, notebook, router or home router. An address the
* model has none for is left to Java's constructor (null).
*/
function exportInterfaces(node: TopologyNode, project: ProjectToExport): FlsInterface[] {
const device = project.deviceConfigurations.get(node.id);
switch (node.kind) {
case 'pc':
case 'notebook':
return node.ports.slice(0, 1).map((port) => ({
ip: device?.ipAddress ?? null,
mask: device?.subnetMask ?? null,
mac: effectiveMacAddress(port),
gateway: device?.defaultGateway ?? '',
dns: device?.dnsServer ?? '',
wireless: wirelessCard(node, project),
}));
case 'router':
case 'gateway':
return node.ports.map((port) => {
const configuration = project.interfaceConfigurations.get(interfaceKey(node.id, port.id));
return {
ip: configuration?.ipAddress ?? null,
mask: configuration?.subnetMask ?? null,
mac: effectiveMacAddress(port),
gateway: device?.defaultGateway ?? '',
dns: device?.dnsServer ?? '',
wireless: false,
};
});
default:
return [];
}
}
/** Components 0…1 as Java's 0…255. */
function channel(value: number): number {
return Math.round(value * 255);
}
function exportDocumentationItem(item: DocumentationItem): FlsDocumentationItem {
return {
type: item.kind === 'rectangle' ? 1 : 2,
text: item.text,
x: Math.round(item.frame.x),
y: Math.round(item.frame.y),
width: Math.round(item.frame.width),
height: Math.round(item.frame.height),
color: [channel(item.color.red), channel(item.color.green), channel(item.color.blue), channel(item.color.alpha)],
font: { name: item.fontName, style: item.isBold ? 1 : 0, size: item.fontSize },
};
}