feat(core): virtual file system and Swift string semantics
The core gains Swift's virtual file system (paths, case-insensitive lookup that keeps the spelling, quotas, atomic changes, the defaults of a new device) and the string behaviour the ported code relies on without saying so. Both are measured in the Swift oracle: 11 file system cases (the three Swift tests among them) step by step, and trimming, lower case, equality, ordering and Base64 case by case — whitespace and control characters over all of Unicode. Found by measuring: - Swift trims NEL and ZERO WIDTH SPACE, not the BOM; JS trim() does the opposite. The .fls import now trims with swiftTrim. - lowercased() maps scalar by scalar: a word-final sigma stays σ. - Foundation decodes "====" and "Y===" as empty data. - Foundation on Linux counts 24,735 plane-14 scalars as control characters, a table artefact; the port keeps Unicode Cc+Cf. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,7 @@ import {
|
||||
inDeterministicRenderOrder,
|
||||
interfaceKey,
|
||||
isSafeFrame,
|
||||
swiftTrim,
|
||||
topologyNode,
|
||||
DOCUMENTATION_BLACK,
|
||||
DOCUMENTATION_PALE_YELLOW,
|
||||
@@ -235,9 +236,14 @@ function importNode(source: FlsNode, newId: () => Id, warnings: string[]): Topol
|
||||
});
|
||||
}
|
||||
|
||||
/** Swift's `value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""`. */
|
||||
function trimmedOrEmpty(value: string | null | undefined): string {
|
||||
return value == null ? '' : swiftTrim(value);
|
||||
}
|
||||
|
||||
/** Swift's normalizedOrDefault; Java's null (a rejected address) counts as empty. */
|
||||
function orDefault(value: string | null, fallback: string): string {
|
||||
const trimmed = value?.trim() ?? '';
|
||||
const trimmed = trimmedOrEmpty(value);
|
||||
return trimmed === '' ? fallback : trimmed;
|
||||
}
|
||||
|
||||
@@ -253,8 +259,8 @@ function registerConfigurations(
|
||||
deviceConfigurations.set(node.id, {
|
||||
ipAddress: orDefault(nic?.ip ?? null, '192.168.0.10'),
|
||||
subnetMask: orDefault(nic?.mask ?? null, '255.255.255.0'),
|
||||
defaultGateway: nic?.gateway?.trim() ?? '',
|
||||
dnsServer: nic?.dns?.trim() ?? '',
|
||||
defaultGateway: trimmedOrEmpty(nic?.gateway),
|
||||
dnsServer: trimmedOrEmpty(nic?.dns),
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -272,14 +278,14 @@ function registerConfigurations(
|
||||
// Swift's model has no router gateway at all; the core reads it from the
|
||||
// router's device configuration, which is set only when there is one.
|
||||
const first = source.interfaces[0];
|
||||
const routerGateway = first?.gateway?.trim() ?? '';
|
||||
const routerGateway = trimmedOrEmpty(first?.gateway);
|
||||
if (node.kind === 'router' && first !== undefined && routerGateway !== '') {
|
||||
const firstConfiguration = interfaceConfigurations.get(interfaceKey(node.id, node.ports[0]!.id));
|
||||
deviceConfigurations.set(node.id, {
|
||||
ipAddress: firstConfiguration?.ipAddress ?? '',
|
||||
subnetMask: firstConfiguration?.subnetMask ?? '',
|
||||
defaultGateway: routerGateway,
|
||||
dnsServer: first.dns?.trim() ?? '',
|
||||
dnsServer: trimmedOrEmpty(first.dns),
|
||||
});
|
||||
}
|
||||
const wan = source.interfaces[0];
|
||||
@@ -288,8 +294,8 @@ function registerConfigurations(
|
||||
deviceConfigurations.set(node.id, {
|
||||
ipAddress: wanConfiguration?.ipAddress ?? '',
|
||||
subnetMask: wanConfiguration?.subnetMask ?? '',
|
||||
defaultGateway: wan.gateway?.trim() ?? '',
|
||||
dnsServer: wan.dns?.trim() ?? '',
|
||||
defaultGateway: trimmedOrEmpty(wan.gateway),
|
||||
dnsServer: trimmedOrEmpty(wan.dns),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -310,7 +316,7 @@ type SystemConfigurations = Pick<
|
||||
/** A row of text cells, trimmed, or undefined unless it has exactly four. */
|
||||
function fourCells(row: (string | null)[]): [string, string, string, string] | undefined {
|
||||
if (row.length !== 4 || row.some((cell) => cell === null)) return undefined;
|
||||
return row.map((cell) => cell!.trim()) as [string, string, string, string];
|
||||
return row.map((cell) => swiftTrim(cell!)) as [string, string, string, string];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -334,10 +340,10 @@ function registerServiceConfigurations(
|
||||
if (server !== undefined) {
|
||||
const configuration = dhcpServerConfiguration({
|
||||
isActive: server.isActive,
|
||||
lowerBoundIPAddress: server.lowerBound?.trim() ?? '',
|
||||
upperBoundIPAddress: server.upperBound?.trim() ?? '',
|
||||
gatewayIPAddress: server.gateway?.trim() ?? '',
|
||||
dnsServerIPAddress: server.dnsServer?.trim() ?? '',
|
||||
lowerBoundIPAddress: trimmedOrEmpty(server.lowerBound),
|
||||
upperBoundIPAddress: trimmedOrEmpty(server.upperBound),
|
||||
gatewayIPAddress: trimmedOrEmpty(server.gateway),
|
||||
dnsServerIPAddress: trimmedOrEmpty(server.dnsServer),
|
||||
useOwnSettings: server.useOwnSettings,
|
||||
staticAssignments: server.staticAssignedAddresses.map((entry) => ({
|
||||
id: newId(),
|
||||
@@ -393,7 +399,7 @@ function registerSystemConfigurations(
|
||||
warnings: string[],
|
||||
): void {
|
||||
const where = `node '${node.displayName}'`;
|
||||
const ssid = source.system.ssid?.trim() ?? '';
|
||||
const ssid = trimmedOrEmpty(source.system.ssid);
|
||||
if (node.kind === 'networkSwitch') {
|
||||
// Without an SSID in the file, Java rolls one; Swift derives it from the node id.
|
||||
system.switchConfigurations.set(node.id, {
|
||||
@@ -435,10 +441,10 @@ function firewallConfigurationOf(firewall: FlsFirewall, where: string, warnings:
|
||||
const protocol = PROTOCOLS[rule.protocol];
|
||||
if (protocol === undefined) warnings.push(`${where}: firewall protocol ${rule.protocol} unknown, used TCP`);
|
||||
return {
|
||||
sourceIPAddress: rule.srcIP?.trim() ?? '',
|
||||
sourceSubnetMask: rule.srcMask?.trim() ?? '',
|
||||
destinationIPAddress: rule.destIP?.trim() ?? '',
|
||||
destinationSubnetMask: rule.destMask?.trim() ?? '',
|
||||
sourceIPAddress: trimmedOrEmpty(rule.srcIP),
|
||||
sourceSubnetMask: trimmedOrEmpty(rule.srcMask),
|
||||
destinationIPAddress: trimmedOrEmpty(rule.destIP),
|
||||
destinationSubnetMask: trimmedOrEmpty(rule.destMask),
|
||||
port: rule.port,
|
||||
protocol: protocol ?? 'tcp',
|
||||
action: action(rule.action, 'action'),
|
||||
|
||||
Reference in New Issue
Block a user