Files
mdg-filius/tools/swift-oracle/Sources/Oracle/main.swift
T
julian.kiedaischandClaude Opus 5.5 c80d212c90 feat(fls): DNS records from /dns/hosts, as Java's server answers them
Java's DNS server reads its records from the text file /dns/hosts on every
query. dnsHosts.ts ports that reading (DNSServer.leseRecordListe,
ResourceRecord, Dateisystem.holeDatei) and keeps in the model the records
Java answers: type exactly A, MX or NS, and a name Java accepted. The file
stays in the file system as it is.

Both oracles read 76 hand-picked hosts files (dns-hosts.cases.txt): the
Java oracle through DNSServer.holeResourceRecords (DnsHostsCases), the
Swift oracle through TopologyDNSHostsFileCodec (--dns-hosts). The Java view
matches Java on every case; the model matches Swift except for 33 named
cases in twelve reasons: Java splits at single spaces and LF only, has no
comments or "IN", turns a TTL that is no int into 3600, keeps duplicates,
and empties names with a leading digit. The Swift dump now carries each
server's records; the corpus comparison covers them on 16 files.

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

700 lines
32 KiB
Swift

// Replays scenario files in the filius-ipad runtime engine and writes what
// happened: every trace event, the result of every step, and the end state.
// The TypeScript port replays the same files (packages/core/test/oracle.test.ts)
// and must produce the same output.
//
// Usage: Oracle <directory> — reads <name>.scenario.json, writes <name>.swift.json
@testable import FiliusCore
import CryptoKit
import Foundation
// MARK: - Scenario format (shared with packages/core/test/oracle/scenario.ts)
struct ScenarioPort: Decodable { let id: String; let label: String; let mac: String? }
struct ScenarioNode: Decodable { let id: String; let kind: String; let ports: [ScenarioPort] }
struct ScenarioLink: Decodable {
let id: String
let sourceNode: String; let sourcePort: String
let targetNode: String; let targetPort: String
}
struct ScenarioDevice: Decodable { let ip: String; let mask: String; let gateway: String?; let dns: String? }
struct ScenarioInterface: Decodable { let node: String; let port: String; let ip: String; let mask: String }
struct ScenarioRoute: Decodable { let destination: String; let mask: String; let gateway: String; let interface: String }
struct ScenarioSwitch: Decodable { let ssid: String; let retention: UInt64? }
struct ScenarioFirewallRule: Decodable {
let source: String?; let sourceMask: String?
let destination: String?; let destinationMask: String?
let port: Int?; let `protocol`: String?; let action: String?
}
struct ScenarioFirewall: Decodable {
let active: Bool?; let defaultPolicy: String?; let dropIcmp: Bool?
let synOnly: Bool?; let filterUdp: Bool?; let rules: [ScenarioFirewallRule]?
}
struct ScenarioStep: Decodable {
let op: String
let from: String?; let node: String?; let target: String?
let ttl: UInt8?; let identifier: UInt16?; let sequence: UInt16?; let maxTtl: UInt8?
let to: UInt64?; let enabled: Bool?; let percent: Int?
}
struct Scenario: Decodable {
let seed: UInt64
let speedPercent: Int?
let nodes: [ScenarioNode]
let links: [ScenarioLink]
let devices: [String: ScenarioDevice]?
let interfaces: [ScenarioInterface]?
let routes: [String: [ScenarioRoute]]?
let switches: [String: ScenarioSwitch]?
let firewalls: [String: ScenarioFirewall]?
let steps: [ScenarioStep]
}
// MARK: - Building the snapshot
func uuid(_ value: String) -> UUID {
guard let id = UUID(uuidString: value) else { fatalError("not a UUID: \(value)") }
return id
}
func firewallAction(_ value: String?) -> TopologyFirewallAction {
value == "accept" ? .accept : .drop
}
func firewallProtocol(_ value: String?) -> TopologyFirewallProtocol {
switch value {
case "all": return .all
case "icmp": return .icmp
case "udp": return .udp
default: return .tcp
}
}
func snapshot(of scenario: Scenario) -> TopologyNetworkRuntimeTopologySnapshot {
let nodes = scenario.nodes.map { node -> TopologyNetworkRuntimeNodeSnapshot in
guard let kind = TopologyNodeKind(rawValue: node.kind) else { fatalError("unknown kind \(node.kind)") }
return TopologyNetworkRuntimeNodeSnapshot(
id: uuid(node.id),
kind: kind,
ports: node.ports.map { TopologyNetworkRuntimePortSnapshot(id: uuid($0.id), label: $0.label, macAddress: $0.mac) }
)
}
let links = scenario.links.map {
TopologyNetworkRuntimeLinkSnapshot(
id: uuid($0.id),
sourceNodeID: uuid($0.sourceNode), sourcePortID: uuid($0.sourcePort),
targetNodeID: uuid($0.targetNode), targetPortID: uuid($0.targetPort)
)
}
var devices: [UUID: TopologyRuntimeDeviceConfiguration] = [:]
for (node, device) in scenario.devices ?? [:] {
devices[uuid(node)] = TopologyRuntimeDeviceConfiguration(
ipAddress: device.ip, subnetMask: device.mask,
defaultGateway: device.gateway ?? "", dnsServer: device.dns ?? ""
)
}
var interfaces: [TopologyRuntimeInterfaceKey: TopologyRuntimeInterfaceConfiguration] = [:]
for item in scenario.interfaces ?? [] {
interfaces[TopologyRuntimeInterfaceKey(nodeID: uuid(item.node), portID: uuid(item.port))] =
TopologyRuntimeInterfaceConfiguration(ipAddress: item.ip, subnetMask: item.mask)
}
var routes: [UUID: [TopologyRuntimeManualRoute]] = [:]
for (node, rows) in scenario.routes ?? [:] {
routes[uuid(node)] = rows.map {
TopologyRuntimeManualRoute(
destinationNetwork: $0.destination, subnetMask: $0.mask,
gateway: $0.gateway, interfaceIPAddress: $0.interface
)
}
}
var switches: [UUID: TopologySwitchConfiguration] = [:]
for (node, config) in scenario.switches ?? [:] {
switches[uuid(node)] = TopologySwitchConfiguration(
ssid: config.ssid,
retentionTimeMilliseconds: config.retention ?? TopologySwitchConfiguration.defaultRetentionTimeMilliseconds
)
}
var firewalls: [UUID: TopologyFirewallConfiguration] = [:]
for (node, config) in scenario.firewalls ?? [:] {
firewalls[uuid(node)] = TopologyFirewallConfiguration(
isActive: config.active ?? false,
defaultPolicy: firewallAction(config.defaultPolicy),
dropICMP: config.dropIcmp ?? false,
filterSYNSegmentsOnly: config.synOnly ?? true,
filterUDP: config.filterUdp ?? true,
rules: (config.rules ?? []).map {
TopologyFirewallRule(
sourceIPAddress: $0.source ?? "", sourceSubnetMask: $0.sourceMask ?? "",
destinationIPAddress: $0.destination ?? "", destinationSubnetMask: $0.destinationMask ?? "",
port: $0.port ?? TopologyFirewallRule.allPorts,
protocolType: firewallProtocol($0.protocol),
action: firewallAction($0.action)
)
}
)
}
return TopologyNetworkRuntimeTopologySnapshot(
nodes: nodes, links: links,
deviceConfigurations: devices, interfaceConfigurations: interfaces,
manualRoutesByNodeID: routes,
firewallConfigurationsByNodeID: firewalls,
switchConfigurationsByNodeID: switches
)
}
// MARK: - Output
typealias JSON = [String: Any]
func headers(_ fields: [TopologyPacketHeaderField]) -> [[String]] {
fields.map { [$0.name, $0.value] }
}
func trace(_ event: TopologyPacketTraceEvent) -> JSON {
var result: JSON = [
"id": event.id,
"time": event.timeMilliseconds,
"node": event.nodeID.uuidString,
"direction": event.direction.rawValue,
"layer": event.layer.rawValue,
"operation": event.operation.rawValue,
"before": headers(event.beforeHeaders),
"after": headers(event.afterHeaders),
]
if let frame = event.frameIdentity { result["frame"] = frame }
if let packet = event.packetIdentity { result["packet"] = packet }
if let interface = event.interfaceID { result["interface"] = interface.uuidString }
if let detail = event.detail { result["detail"] = detail }
return result
}
func delivery(_ result: TopologyIPv4DeliveryResult) -> JSON {
switch result {
case let .delivered(packet, node): return ["type": "delivered", "packet": packet, "node": node.uuidString]
case let .icmpError(packet, node, kind):
return ["type": "icmpError", "packet": packet, "node": node.uuidString, "kind": kind.rawValue]
case let .dropped(packet, node):
var json: JSON = ["type": "dropped", "packet": packet]
if let node { json["node"] = node.uuidString }
return json
}
}
func observation(_ item: TopologyRuntimeICMPObservation) -> JSON {
[
"kind": item.message.kind.rawValue,
"identifier": item.message.identifier,
"sequence": item.message.sequenceNumber,
"sender": item.packet.senderIPAddress,
]
}
func run(_ scenario: Scenario) -> JSON {
var engine = TopologyNetworkRuntimeEngine(seed: scenario.seed)
if let percent = scenario.speedPercent { engine.setSimulationSpeed(percent: percent) }
engine.handle(.start(snapshot: snapshot(of: scenario), seed: scenario.seed, initialTimeMilliseconds: 0))
var results: [Any] = []
for step in scenario.steps {
switch step.op {
case "ping":
results.append(delivery(engine.sendICMPEcho(
fromNodeID: uuid(step.from!), targetIPAddress: step.target!,
timeToLive: step.ttl ?? 64, identifier: step.identifier ?? 1, sequenceNumber: step.sequence ?? 1
)))
case "traceroute":
results.append(engine.traceICMPEcho(
fromNodeID: uuid(step.from!), targetIPAddress: step.target!,
maximumTimeToLive: step.maxTtl ?? 64, identifier: step.identifier ?? 1
).map(observation))
case "resolveMac":
results.append(engine.resolveMACAddress(nodeID: uuid(step.node!), targetIPAddress: step.target!) ?? NSNull())
case "advance":
engine.handle(.advance(toMilliseconds: step.to!))
results.append(engine.state.currentTimeMilliseconds)
case "packetLoss":
engine.setGlobalPacketLossEnabled(step.enabled!)
results.append(engine.state.globalPacketLossEnabled)
case "sat":
results.append(engine.switchSATEntries(nodeID: uuid(step.node!)).map {
["mac": $0.macAddress, "port": $0.portLabel, "updated": $0.updatedAtMilliseconds] as JSON
})
case "speed":
engine.setSimulationSpeed(percent: step.percent!)
results.append(engine.simulationSpeed.percent)
default:
fatalError("unknown step \(step.op)")
}
}
let state = engine.state
var arp: JSON = [:]
for (node, cache) in state.arpCachesByNodeID {
arp[node.uuidString] = cache.mapValues { $0.macAddress }
}
var switchTables: JSON = [:]
for (node, table) in state.switchForwardingTablesByNodeID {
switchTables[node.uuidString] = table.mapValues { $0.uuidString }
}
var observations: JSON = [:]
for (node, items) in state.icmpObservationsByNodeID {
observations[node.uuidString] = items.map(observation)
}
return [
"results": results,
"traces": state.packetTraces.map(trace),
"time": state.currentTimeMilliseconds,
"pendingEvents": state.pendingEvents.count,
"arp": arp,
"switchTables": switchTables,
"icmpObservations": observations,
"firewallDecisions": state.firewallDecisions.map { decision -> JSON in
var json: JSON = [
"packet": decision.packetIdentity, "node": decision.nodeID.uuidString, "accepted": decision.accepted,
]
if let rule = decision.ruleIndex { json["rule"] = rule }
return json
},
]
}
// MARK: - .fls corpus
/// Imports every .fls under `root` and summarizes what the Swift reader made of it.
func readCorpus(root: URL) throws -> JSON {
var result: JSON = [:]
let enumerator = FileManager.default.enumerator(at: root, includingPropertiesForKeys: nil)
var files: [URL] = []
while let item = enumerator?.nextObject() as? URL {
if item.pathExtension == "fls" { files.append(item) }
}
for file in files.sorted(by: { $0.path < $1.path }) {
let name = String(file.path.dropFirst(root.path.count + 1))
do {
let imported = try TopologyProjectStore.importFiliusArchive(Data(contentsOf: file))
let state = imported.state
var kinds: [String: Int] = [:]
for node in state.graph.nodes { kinds[node.kind.rawValue, default: 0] += 1 }
result[name] = [
"version": imported.report.filiusVersion ?? NSNull(),
"nodes": imported.report.importedNodeCount,
"links": imported.report.importedLinkCount,
"documentation": imported.report.importedDocumentationItemCount,
"skipped": imported.report.skippedNodeCount,
"warnings": imported.report.warnings,
"residual": imported.opaqueContent.residualCount,
"kinds": kinds,
"state": describeImportedState(state),
] as JSON
} catch {
result[name] = ["error": String(describing: error)] as JSON
}
}
return result
}
/// The imported model without its random UUIDs: nodes, ports and links by
/// index; the IP configurations the runtime starts from; documentation items
/// with their deterministic ids.
func describeImportedState(_ state: TopologyEditorState) -> JSON {
let nodeIndex = Dictionary(uniqueKeysWithValues: state.graph.nodes.enumerated().map { ($1.id, $0) })
func portIndex(_ nodeID: UUID, _ portID: UUID) -> Int {
state.graph.node(withID: nodeID)?.ports.firstIndex { $0.id == portID } ?? -1
}
let nodes: [JSON] = state.graph.nodes.map { node in
let device: Any = state.runtimeDeviceConfigurations[node.id].map { configuration -> JSON in
[
"ipAddress": configuration.ipAddress,
"subnetMask": configuration.subnetMask,
"defaultGateway": configuration.defaultGateway,
"dnsServer": configuration.dnsServer,
]
} ?? NSNull()
let interfaces: [Any] = node.ports.map { port in
state.runtimeInterfaceConfigurations[TopologyRuntimeInterfaceKey(nodeID: node.id, portID: port.id)]
.map { ["ipAddress": $0.ipAddress, "subnetMask": $0.subnetMask] as JSON } ?? NSNull()
}
return [
"kind": node.kind.rawValue,
"displayName": node.displayName,
"hostLabelPolicy": node.hostLabelPolicy.rawValue,
"position": [Double(node.position.x), Double(node.position.y)],
"ports": node.ports.map { port -> JSON in
["label": port.label, "mac": port.importedMACAddress ?? NSNull(), "occupied": port.isOccupied]
},
"device": device,
"interfaces": interfaces,
"system": describeSystem(state, node),
"fileSystem": state.virtualFileSystemsByNodeID[node.id].map { describeFileEntries($0.allEntries()) } ?? NSNull(),
]
}
let links: [[Int]] = state.graph.links.map { link in
[
nodeIndex[link.sourceNodeID] ?? -1, portIndex(link.sourceNodeID, link.sourcePortID),
nodeIndex[link.targetNodeID] ?? -1, portIndex(link.targetNodeID, link.targetPortID),
]
}
let documentation: [JSON] = state.documentationItems.map { item in
[
"id": item.id.uuidString,
"kind": item.kind.rawValue,
"frame": [item.frame.origin.x, item.frame.origin.y, item.frame.width, item.frame.height].map { Double($0) },
"text": item.text,
"color": [item.color.red, item.color.green, item.color.blue, item.color.alpha],
"fontName": item.fontName,
"fontSize": Double(item.fontSize),
"isBold": item.isBold,
"order": item.order,
]
}
return ["nodes": nodes, "links": links, "documentation": documentation]
}
/// The system software settings of a node in the imported state. A switch
/// SSID Swift derives from the (random) node id is written as a marker.
func describeSystem(_ state: TopologyEditorState, _ node: TopologyNode) -> JSON {
var json: JSON = [:]
if let configuration = state.switchConfigurationsByNodeID[node.id] {
let derived = TopologySwitchConfiguration.defaultConfiguration(nodeID: node.id).ssid
json["switch"] = [
"ssid": configuration.ssid == derived ? "<from node id>" : configuration.ssid,
"retentionTimeMilliseconds": Int(configuration.retentionTimeMilliseconds),
] as JSON
}
if let wireless = state.hostWirelessConfigurationsByNodeID[node.id] {
json["hostWireless"] = ["isEnabled": wireless.isEnabled, "ssid": wireless.ssid] as JSON
}
if let routes = state.runtimeManualRoutesByNodeID[node.id] {
json["manualRoutes"] = routes.map { [$0.destinationNetwork, $0.subnetMask, $0.gateway, $0.interfaceIPAddress] }
}
if let rip = state.runtimeRIPEnabledByNodeID[node.id] { json["ripEnabled"] = rip }
if let client = state.runtimeDHCPClientConfigurationsByNodeID[node.id] { json["dhcpClient"] = client.isEnabled }
if let server = state.runtimeDHCPServerConfigurationsByNodeID[node.id] {
json["dhcpServer"] = [
"isActive": server.isActive,
"lowerBoundIPAddress": server.lowerBoundIPAddress,
"upperBoundIPAddress": server.upperBoundIPAddress,
"gatewayIPAddress": server.gatewayIPAddress,
"dnsServerIPAddress": server.dnsServerIPAddress,
"useOwnSettings": server.useOwnSettings,
// The assignments' ids are random.
"staticAssignments": server.staticAssignments.map { [$0.macAddress, $0.ipAddress] },
] as JSON
}
if let rows = state.runtimePortForwardingRowsByNodeID[node.id] {
json["portForwarding"] = rows.map { [$0.protocolValue, $0.publicPortValue, $0.lanIPAddress, $0.lanPortValue] }
}
if let programs = state.runtimeInstalledProgramsByNodeID[node.id] {
json["installedPrograms"] = programs.map(\.rawValue).sorted()
}
if let web = state.runtimeWebServerConfigurationsByNodeID[node.id] {
json["webServer"] = [
"port": web.port,
"documentRoot": web.documentRoot,
"virtualHosts": web.virtualHostConfiguration != nil,
] as JSON
}
if let gnutella = state.runtimeGnutellaConfigurationsByNodeID[node.id] {
json["gnutella"] = ["maximumKnownPeers": gnutella.maximumKnownPeers] as JSON
}
if let dns = state.runtimeDNSServerConfigurationsByNodeID[node.id] {
json["dnsServer"] = [
"recursiveResolutionEnabled": dns.recursiveResolutionEnabled,
"forwardingServerIPAddress": dns.forwardingServerIPAddress ?? NSNull(),
"records": dns.typedRecords.map { dnsRecordJSON($0) },
] as JSON
}
if let link = state.remoteLinkConfigurationsByNodeID[node.id] {
json["remoteLink"] = [
"pairIdentifier": link.pairIdentifier,
"latencyMilliseconds": Int(link.latencyMilliseconds),
"isEnabled": link.isEnabled,
] as JSON
}
if let firewall = state.runtimeFirewallConfigurationsByNodeID[node.id] {
json["firewall"] = [
"isActive": firewall.isActive,
"defaultPolicy": firewall.defaultPolicy.rawValue,
"dropICMP": firewall.dropICMP,
"filterSYNSegmentsOnly": firewall.filterSYNSegmentsOnly,
"filterUDP": firewall.filterUDP,
"rules": firewall.rules.map { rule -> [Any] in
[rule.sourceIPAddress, rule.sourceSubnetMask, rule.destinationIPAddress, rule.destinationSubnetMask,
rule.port, rule.protocolType.rawValue, rule.action.rawValue]
},
] as JSON
}
return json
}
// MARK: - Virtual file system
/// One step of a file system case (packages/core/test/vfs.cases.json).
struct FileSystemStep: Decodable {
let op: String
let path: String?
let to: String?
let name: String?
let text: String?
let size: Int?
let byte: UInt8?
let mediaType: String?
let recursive: Bool?
let overwrite: Bool?
let entries: [FileSystemCaseEntry]?
}
struct FileSystemCaseEntry: Decodable { let path: String; let kind: String; let text: String? }
struct FileSystemCase: Decodable { let name: String; let steps: [FileSystemStep] }
func describeFileSystemError(_ error: Error) -> JSON {
guard let error = error as? TopologyVirtualFileSystemError else {
return ["error": "other", "values": [String(describing: error)]]
}
func result(_ code: String, _ values: [Any] = []) -> JSON { ["error": code, "values": values] }
switch error {
case let .pathMustBeAbsolute(path): return result("pathMustBeAbsolute", [path])
case let .pathEscapesRoot(path): return result("pathEscapesRoot", [path])
case let .invalidPathComponent(component): return result("invalidPathComponent", [component])
case let .itemAlreadyExists(path): return result("itemAlreadyExists", [path])
case let .caseInsensitiveSiblingCollision(existing, attempted):
return result("caseInsensitiveSiblingCollision", [existing, attempted])
case let .itemNotFound(path): return result("itemNotFound", [path])
case let .parentDirectoryNotFound(path): return result("parentDirectoryNotFound", [path])
case let .expectedDirectory(path): return result("expectedDirectory", [path])
case let .expectedFile(path): return result("expectedFile", [path])
case let .expectedTextFile(path): return result("expectedTextFile", [path])
case let .expectedImageFile(path): return result("expectedImageFile", [path])
case let .directoryNotEmpty(path): return result("directoryNotEmpty", [path])
case .cannotMutateRoot: return result("cannotMutateRoot")
case let .cannotMoveDirectoryIntoItself(source, destination):
return result("cannotMoveDirectoryIntoItself", [source, destination])
case let .fileSizeQuotaExceeded(path, actual, limit): return result("fileSizeQuotaExceeded", [path, actual, limit])
case let .deviceSizeQuotaExceeded(actual, limit): return result("deviceSizeQuotaExceeded", [actual, limit])
case let .deviceEntryQuotaExceeded(actual, limit): return result("deviceEntryQuotaExceeded", [actual, limit])
case let .totalSizeQuotaExceeded(actual, limit): return result("totalSizeQuotaExceeded", [actual, limit])
case let .totalEntryQuotaExceeded(actual, limit): return result("totalEntryQuotaExceeded", [actual, limit])
}
}
func describeFileEntries(_ entries: [TopologyVirtualFileEntry]) -> [JSON] {
entries.map { entry in
var json: JSON = ["path": entry.path]
switch entry.content {
case .directory: json["kind"] = "directory"
case let .text(value): json["kind"] = "text"; json["text"] = value
case let .binary(data, mediaType):
json["kind"] = "binary"; json["bytes"] = data.count; json["mediaType"] = mediaType ?? NSNull()
json["sha256"] = SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined()
case let .image(data, mediaType):
json["kind"] = "image"; json["bytes"] = data.count; json["mediaType"] = mediaType
json["sha256"] = SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined()
}
return json
}
}
func runFileSystemCases(_ cases: [FileSystemCase]) -> [JSON] {
cases.map { item in
var fileSystem = TopologyVirtualFileSystem()
var results: [JSON] = []
for step in item.steps {
do {
let path = step.path ?? "/"
switch step.op {
case "createDirectory": try fileSystem.createDirectory(at: path, recursive: step.recursive ?? false)
case "writeText": try fileSystem.writeTextFile(at: path, text: step.text ?? "", overwrite: step.overwrite ?? true)
case "writeBinary":
try fileSystem.writeBinaryFile(
at: path, data: Data(repeating: step.byte ?? 0, count: step.size ?? 0),
mediaType: step.mediaType, overwrite: step.overwrite ?? true)
case "writeImage":
try fileSystem.writeImageFile(
at: path, data: Data(repeating: step.byte ?? 0, count: step.size ?? 0),
mediaType: step.mediaType ?? "image/png", overwrite: step.overwrite ?? true)
case "copy": try fileSystem.copyItem(at: path, to: step.to ?? "/", overwrite: step.overwrite ?? false)
case "move": try fileSystem.moveItem(at: path, to: step.to ?? "/", overwrite: step.overwrite ?? false)
case "rename": results.append(["ok": try fileSystem.renameItem(at: path, to: step.name ?? "")]); continue
case "delete": try fileSystem.deleteItem(at: path, recursive: step.recursive ?? false)
case "entry": results.append(["ok": describeFileEntries([try fileSystem.entry(at: path)])[0]]); continue
case "entries": results.append(["ok": describeFileEntries(try fileSystem.entries(in: path))]); continue
case "text": results.append(["ok": try fileSystem.textFile(at: path)]); continue
case "contains": results.append(["ok": fileSystem.contains(path)]); continue
case "normalize": results.append(["ok": try TopologyVirtualFileSystem.normalizedAbsolutePath(path)]); continue
case "default": fileSystem = .defaultForDevice()
case "fromEntries":
fileSystem = try TopologyVirtualFileSystem(entries: (step.entries ?? []).map { entry in
TopologyVirtualFileEntry(
path: entry.path,
content: entry.kind == "directory" ? .directory : .text(entry.text ?? ""))
})
default: results.append(["error": "unknown step \(step.op)"]); continue
}
results.append(["ok": true])
} catch {
results.append(describeFileSystemError(error))
}
}
return ["name": item.name, "results": results, "entries": describeFileEntries(fileSystem.allEntries())]
}
}
// MARK: - String semantics
struct StringCases: Decodable {
let trim: [String]
let lowercased: [String]
let equal: [[String]]
let sort: [[String]]
let base64: [String]
let controlCharacters: [String]
let pathExtension: [String]
}
/// What Swift makes of strings where JavaScript differs (packages/core/test/strings.cases.json).
func measureStrings(_ cases: StringCases) -> JSON {
[
"trim": cases.trim.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) },
"lowercased": cases.lowercased.map { $0.lowercased() },
"equal": cases.equal.map { $0[0] == $0[1] },
"sort": cases.sort.map { $0.sorted() },
"base64": cases.base64.map { text -> Any in
Data(base64Encoded: text, options: .ignoreUnknownCharacters).map { Array($0) } ?? NSNull()
},
"controlCharacters": cases.controlCharacters.map { text in
text.unicodeScalars.contains(where: { CharacterSet.controlCharacters.contains($0) })
},
"pathExtension": cases.pathExtension.map { ($0 as NSString).pathExtension },
// The whole sets, scalar by scalar, as ranges [first, last].
"whitespacesAndNewlinesSet": scalarRanges(in: .whitespacesAndNewlines),
"controlCharactersSet": scalarRanges(in: .controlCharacters),
]
}
func scalarRanges(in set: CharacterSet) -> [[Int]] {
var ranges: [[Int]] = []
for value in 0...0x10FFFF {
guard let scalar = Unicode.Scalar(value), set.contains(scalar) else { continue }
if let last = ranges.last, last[1] == value - 1 {
ranges[ranges.count - 1][1] = value
} else {
ranges.append([value, value])
}
}
return ranges
}
// MARK: - Archive path folding
struct PathCase: Decodable { let name: String; let first: String; let second: String }
/// Whether Swift's archive writer treats two entry paths as the same file.
/// The folding itself is file-private in Swift; the export reports it as a
/// duplicate entry when two carried entries collide.
func foldPaths(_ cases: [PathCase]) -> [JSON] {
cases.map { item in
var result: JSON = ["name": item.name]
// Swift compares strings by canonical equivalence: "é" and "e\u{301}"
// are one dictionary key, so they never reach the archive writer.
if item.first == item.second {
result["outcome"] = "sameKey"
return result
}
do {
_ = try TopologyProjectStore.exportFiliusArchiveWithReport(
from: TopologyEditorState(),
supplementalEntries: [item.first: Data("1".utf8), item.second: Data("2".utf8)]
)
result["outcome"] = "distinct"
} catch let error as TopologyFLSArchiveError {
result["outcome"] = error.code == .duplicateEntry ? "collide" : error.code.rawValue
} catch {
result["outcome"] = "error: \(error)"
}
return result
}
}
// MARK: - /dns/hosts cases (packages/fls/test/dns-hosts.cases.txt)
func dnsRecordJSON(_ record: TopologyDNSResourceRecord) -> [Any] {
[record.name.rawValue, record.type.rawValue, Int(record.ttlSeconds), record.target]
}
/// One case per line: a whole hosts file, ASCII with Java-style unicode
/// escapes (backslash, u, four hex digits) — the file the Java oracle reads.
func readDNSHostsCases(_ url: URL) throws -> [(line: String, text: String)] {
let lines = try String(contentsOf: url, encoding: .ascii).components(separatedBy: "\n")
return lines.dropLast().map { line in
var units: [UInt16] = []
var rest = Substring(line)
while let first = rest.first {
if rest.hasPrefix("\\u"), rest.count >= 6, let unit = UInt16(rest.dropFirst(2).prefix(4), radix: 16) {
units.append(unit)
rest = rest.dropFirst(6)
} else {
units.append(contentsOf: String(first).utf16)
rest = rest.dropFirst()
}
}
return (line, String(decoding: units, as: UTF16.self))
}
}
// MARK: - Main
if CommandLine.arguments.count == 4, CommandLine.arguments[1] == "--path-folding" {
let cases = try JSONDecoder().decode([PathCase].self, from: Data(contentsOf: URL(fileURLWithPath: CommandLine.arguments[2])))
let data = try JSONSerialization.data(withJSONObject: foldPaths(cases), options: [.prettyPrinted, .sortedKeys])
try (data + Data("\n".utf8)).write(to: URL(fileURLWithPath: CommandLine.arguments[3]))
print("path folding: \(cases.count) cases")
exit(0)
}
if CommandLine.arguments.count == 4, CommandLine.arguments[1] == "--vfs" {
let cases = try JSONDecoder().decode([FileSystemCase].self, from: Data(contentsOf: URL(fileURLWithPath: CommandLine.arguments[2])))
let output = runFileSystemCases(cases)
let data = try JSONSerialization.data(withJSONObject: output, options: [.prettyPrinted, .sortedKeys])
try (data + Data("\n".utf8)).write(to: URL(fileURLWithPath: CommandLine.arguments[3]))
print("vfs: \(output.count) cases")
exit(0)
}
if CommandLine.arguments.count == 4, CommandLine.arguments[1] == "--strings" {
let cases = try JSONDecoder().decode(StringCases.self, from: Data(contentsOf: URL(fileURLWithPath: CommandLine.arguments[2])))
let data = try JSONSerialization.data(withJSONObject: measureStrings(cases), options: [.prettyPrinted, .sortedKeys])
try (data + Data("\n".utf8)).write(to: URL(fileURLWithPath: CommandLine.arguments[3]))
print("strings: measured")
exit(0)
}
if CommandLine.arguments.count == 4, CommandLine.arguments[1] == "--dns-hosts" {
let cases = try readDNSHostsCases(URL(fileURLWithPath: CommandLine.arguments[2]))
let output: [[Any]] = cases.map { [$0.line, TopologyDNSHostsFileCodec.records(from: $0.text).map { dnsRecordJSON($0) }] }
let data = try JSONSerialization.data(withJSONObject: output, options: [.prettyPrinted, .sortedKeys])
try (data + Data("\n".utf8)).write(to: URL(fileURLWithPath: CommandLine.arguments[3]))
print("dns hosts: \(cases.count) cases")
exit(0)
}
if CommandLine.arguments.count == 4, CommandLine.arguments[1] == "--corpus" {
let summary = try readCorpus(root: URL(fileURLWithPath: CommandLine.arguments[2]))
let data = try JSONSerialization.data(withJSONObject: summary, options: [.prettyPrinted, .sortedKeys])
try (data + Data("\n".utf8)).write(to: URL(fileURLWithPath: CommandLine.arguments[3]))
print("corpus: \(summary.count) files")
exit(0)
}
let directory = URL(fileURLWithPath: CommandLine.arguments.dropFirst().first ?? ".")
let files = try FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil)
.filter { $0.lastPathComponent.hasSuffix(".scenario.json") }
.sorted { $0.lastPathComponent < $1.lastPathComponent }
for file in files {
let scenario = try JSONDecoder().decode(Scenario.self, from: Data(contentsOf: file))
let output = run(scenario)
let data = try JSONSerialization.data(withJSONObject: output, options: [.prettyPrinted, .sortedKeys])
let name = file.lastPathComponent.replacingOccurrences(of: ".scenario.json", with: ".swift.json")
try (data + Data("\n".utf8)).write(to: directory.appendingPathComponent(name))
print("\(name): \((output["traces"] as? [Any])?.count ?? 0) trace events")
}