ExampleRun opens an example as FILIUS opens it (SzenarioVerwaltung.laden into the window's lists), starts it as the action mode does, plays a story and writes every card's frames and tables, the calls, the addresses and the RIP tables' changes. examplesJava.test.ts reads the same file with this package's reader and runs it in the core (projectSnapshot, new): routing (internetwork_komplett), DHCP (einfaches_rechnernetz_komplett), DNS (dns_server), NAT (nat_with_mapped_port) and firewall (Internet_Komplett_..._DE) match card by card. Found and fixed: a host's ipForwardingEnabled, which Java saves and obeys, was unknown to the reader, the model, the core, the writer, the export and FlsOracle; and Java's ARP learns only from a request for its own address or a reply. Named Java bugs: a ping without a gateway throws a NullPointerException; a home router drops the answer of a second remote end. Open: Java picks between equally long RIP routes by thread timing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
153 lines
4.8 KiB
TypeScript
153 lines
4.8 KiB
TypeScript
// Ported from filius-ipad (GPLv2/GPLv3):
|
|
// ios/FiliusPad/TopologyEditor/Model/TopologyNode.swift
|
|
// Java counterpart: filius.hardware.knoten.* (port counts per device kind).
|
|
|
|
import type { Id } from '../ids.js';
|
|
|
|
export type NodeKind =
|
|
| 'pc'
|
|
| 'notebook'
|
|
| 'networkSwitch'
|
|
| 'router'
|
|
| 'gateway'
|
|
| 'remoteLink'
|
|
| 'unsupported';
|
|
|
|
/** Whether a node of this kind forwards packets unless its settings say otherwise: routers and home routers (Java's constructors). */
|
|
export function forwardsIPv4ByDefault(kind: NodeKind): boolean {
|
|
return kind === 'router' || kind === 'gateway';
|
|
}
|
|
|
|
export function isPcClassEndpoint(kind: NodeKind): boolean {
|
|
return kind === 'pc' || kind === 'notebook';
|
|
}
|
|
|
|
export type HostLabelPolicy = 'manual' | 'ipAddress' | 'macAddress' | 'ipAndMAC';
|
|
|
|
export interface PortMetadata {
|
|
readonly id: Id;
|
|
label: string;
|
|
isOccupied: boolean;
|
|
/** Canonical (upper case, colon separated) or undefined. */
|
|
readonly importedMacAddress?: string;
|
|
}
|
|
|
|
/**
|
|
* The MAC address of a port nobody gave one: `02:` followed by the last ten
|
|
* hex digits of the port's UUID — stable across saves, locally administered.
|
|
*/
|
|
export function stableGeneratedMacAddress(portId: Id): string {
|
|
const compact = portId.toUpperCase().replaceAll('-', '');
|
|
const suffix = compact.slice(-10);
|
|
const octets = ['02'];
|
|
for (let offset = 0; offset < suffix.length; offset += 2) {
|
|
octets.push(suffix.slice(offset, offset + 2).toUpperCase());
|
|
}
|
|
return octets.join(':');
|
|
}
|
|
|
|
/** Six colon-separated pairs of hex digits, upper-cased; anything else is undefined. */
|
|
export function canonicalMacAddress(value: string | undefined | null): string | undefined {
|
|
if (value === undefined || value === null) return undefined;
|
|
const segments = value.trim().split(':');
|
|
if (segments.length !== 6) return undefined;
|
|
if (!segments.every((segment) => /^[0-9A-Fa-f]{2}$/.test(segment))) return undefined;
|
|
return segments.map((segment) => segment.toUpperCase()).join(':');
|
|
}
|
|
|
|
export function effectiveMacAddress(port: PortMetadata): string {
|
|
return port.importedMacAddress ?? stableGeneratedMacAddress(port.id);
|
|
}
|
|
|
|
/**
|
|
* The name a new device gets. These are the Java FILIUS device names; the
|
|
* editor shows them translated (packages/editor i18n), the model keeps them
|
|
* as they appear in a Java .fls file.
|
|
*/
|
|
export function defaultDisplayName(kind: NodeKind): string {
|
|
switch (kind) {
|
|
case 'pc':
|
|
return 'Rechner';
|
|
case 'notebook':
|
|
return 'Notebook';
|
|
case 'networkSwitch':
|
|
return 'Switch';
|
|
case 'router':
|
|
return 'Vermittlungsrechner';
|
|
case 'gateway':
|
|
return 'Gateway';
|
|
case 'remoteLink':
|
|
return 'Remote Link';
|
|
case 'unsupported':
|
|
return 'Unbekanntes Gerät';
|
|
}
|
|
}
|
|
|
|
/** Labels of the ports a new device of this kind has, in order. */
|
|
export function defaultPortLabels(kind: NodeKind): string[] {
|
|
switch (kind) {
|
|
case 'pc':
|
|
case 'notebook':
|
|
return ['eth0'];
|
|
case 'networkSwitch':
|
|
// Java Switch constructs exactly 24 physical ports.
|
|
return Array.from({ length: 24 }, (_, index) => `sw${index + 1}`);
|
|
case 'router':
|
|
// Java Vermittlungsrechner starts with one configurable network interface.
|
|
return ['rt1'];
|
|
case 'gateway':
|
|
// Java Gateway has fixed WAN (index 0) and LAN (index 1) interfaces.
|
|
return ['wan0', 'lan0'];
|
|
case 'remoteLink':
|
|
// One local physical attachment, matching the Java Modem.
|
|
return ['remote0'];
|
|
case 'unsupported':
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export interface Position {
|
|
readonly x: number;
|
|
readonly y: number;
|
|
}
|
|
|
|
export interface TopologyNode {
|
|
readonly id: Id;
|
|
kind: NodeKind;
|
|
displayName: string;
|
|
hostLabelPolicy: HostLabelPolicy;
|
|
position: Position;
|
|
ports: PortMetadata[];
|
|
}
|
|
|
|
/**
|
|
* A node as Swift's `TopologyNode.init` builds it: only a PC or notebook
|
|
* shows its IP or MAC address as its label, and a new node gets the ports of
|
|
* its kind.
|
|
*
|
|
* Unlike Swift, a given name is kept as it is — also empty: Java FILIUS
|
|
* files leave routers deliberately unnamed (the Internet examples), and Java
|
|
* shows them without a label. Only a node without a name gets the default.
|
|
*/
|
|
export function topologyNode(fields: {
|
|
id: Id;
|
|
kind: NodeKind;
|
|
displayName?: string;
|
|
hostLabelPolicy?: HostLabelPolicy;
|
|
position: Position;
|
|
ports?: PortMetadata[];
|
|
/** Makes the ids of the default ports; the core has no randomness of its own. */
|
|
newId: () => Id;
|
|
}): TopologyNode {
|
|
return {
|
|
id: fields.id,
|
|
kind: fields.kind,
|
|
displayName: fields.displayName ?? defaultDisplayName(fields.kind),
|
|
hostLabelPolicy: isPcClassEndpoint(fields.kind) ? (fields.hostLabelPolicy ?? 'manual') : 'manual',
|
|
position: fields.position,
|
|
ports:
|
|
fields.ports ??
|
|
defaultPortLabels(fields.kind).map((label) => ({ id: fields.newId(), label, isOccupied: false })),
|
|
};
|
|
}
|