// 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 })), }; }