// 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 — reads .scenario.json, writes .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? // UDP: a socket is named by the id bindUdp returned. let port: UInt16?; let localIp: String?; let remoteIp: String?; let remotePort: UInt16? let socket: String?; let payload: String?; let timeout: UInt64? } 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) case "bindUdp": results.append(engine.bindUDPSocket( nodeID: uuid(step.node!), localPort: step.port, localIPAddress: step.localIp, remoteIPAddress: step.remoteIp, remotePort: step.remotePort )?.uuidString ?? NSNull()) case "sendUdp": results.append(engine.sendUDP( socketID: uuid(step.socket!), payload: Data((step.payload ?? "").utf8), destinationIPAddress: step.target, destinationPort: step.port ).map(delivery) ?? NSNull()) case "receiveUdp": let received = step.timeout.map { engine.receiveUDP(socketID: uuid(step.socket!), timeoutMilliseconds: $0) } ?? engine.receiveUDP(socketID: uuid(step.socket!)) results.append(received.map { item -> JSON in [ "sender": item.senderIPAddress, "receiver": item.receiverIPAddress, "sourcePort": item.datagram.sourcePort, "destinationPort": item.datagram.destinationPort, "payload": String(decoding: item.datagram.payload, as: UTF8.self), "at": item.receivedAtMilliseconds, ] } ?? NSNull()) case "closeSocket": engine.closeSocket(socketID: uuid(step.socket!)) results.append(engine.state.socketsByID[uuid(step.socket!)] == nil) 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, "sockets": state.socketsByID.values.sorted { $0.id.uuidString < $1.id.uuidString }.map { socket -> JSON in [ "id": socket.id.uuidString, "node": socket.nodeID.uuidString, "protocol": socket.protocolKind.rawValue, "localIp": socket.localIPAddress ?? NSNull(), "localPort": socket.localPort, "remoteIp": socket.remoteIPAddress ?? NSNull(), "remotePort": socket.remotePort.map { $0 as Any } ?? NSNull(), "queued": state.udpReceiveQueuesBySocketID[socket.id]?.count ?? 0, ] }, "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 ? "" : 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 client = state.runtimeEmailClientConfigurationsByNodeID[node.id] { json["emailClient"] = [ "pop3Host": client.pop3Host, "pop3Port": client.pop3Port, "smtpHost": client.smtpHost, "smtpPort": client.smtpPort, "username": client.username, "password": client.password, "name": client.name, "email": client.email, "inbox": client.inbox.map(emailMessageJSON), "sent": client.sent.map(emailMessageJSON), "drafts": client.drafts.map(emailMessageJSON), "nextMessageID": Int(client.nextMessageID), ] as JSON } if let server = state.runtimeEmailServerConfigurationsByNodeID[node.id] { json["emailServer"] = [ "domain": server.domain, "pop3Port": server.pop3Port, "accounts": server.accounts.map { account in [ "username": account.username, "password": account.password, "name": account.name, "mailbox": account.mailbox.map(emailMessageJSON), ] as JSON }, "nextMessageID": Int(server.nextMessageID), ] 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 } } func emailAddressJSON(_ address: TopologyRuntimeEmailAddress) -> [Any] { [address.name ?? NSNull(), address.mailAddress] } func emailMessageJSON(_ message: TopologyRuntimeEmailMessage) -> JSON { [ "id": Int(message.id), "from": emailAddressJSON(message.from), "to": message.to.map(emailAddressJSON), "cc": message.cc.map(emailAddressJSON), "bcc": message.bcc.map(emailAddressJSON), "subject": message.subject, "body": message.body, "deliveryIdentity": message.deliveryIdentity ?? NSNull(), "receivedAtMilliseconds": message.receivedAtMilliseconds.map { Int($0) } ?? NSNull(), "isNew": message.isNew, "isMarkedForDeletion": message.isMarkedForDeletion, "isSent": message.isSent, ] } // 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 file (hosts, e-mail accounts), 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 == 5, CommandLine.arguments[1] == "--email-files" { let server = CommandLine.arguments[2] == "server" let cases = try readDNSHostsCases(URL(fileURLWithPath: CommandLine.arguments[3])) let output: [[Any]] = cases.map { item in do { if server { let configuration = try TopologyRuntimeEmailStorage.decodeJavaServer(item.text) return [item.line, configuration.accounts.map { account in [ "username": account.username, "password": account.password, "name": account.name, "mailbox": account.mailbox.map(emailMessageJSON), ] as JSON }] } let client = try TopologyRuntimeEmailStorage.decodeJavaClient(item.text) return [item.line, [ "pop3Host": client.pop3Host, "pop3Port": client.pop3Port, "smtpHost": client.smtpHost, "smtpPort": client.smtpPort, "username": client.username, "password": client.password, "name": client.name, "email": client.email, ] as JSON] } catch { return [item.line, "!\(error)"] } } let data = try JSONSerialization.data(withJSONObject: output, options: [.prettyPrinted, .sortedKeys]) try (data + Data("\n".utf8)).write(to: URL(fileURLWithPath: CommandLine.arguments[4])) print("email \(CommandLine.arguments[2]) files: \(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") }