199 lines
9.9 KiB
Swift
199 lines
9.9 KiB
Swift
import Foundation
|
|
|
|
func values(_ input: String) -> [String: String] {
|
|
var result: [String: String] = [:]
|
|
for item in input.split(separator: ";") {
|
|
let pair = item.split(separator: "=", maxSplits: 1).map(String.init)
|
|
if pair.count == 2 { result[pair[0]] = pair[1] }
|
|
}
|
|
return result
|
|
}
|
|
|
|
func evaluate(_ kind: String, _ input: String) -> String {
|
|
let values = values(input)
|
|
switch kind {
|
|
case "version": return ["1", "0", "-1"].contains(input) ? "valid" : "invalid:unsupported_version"
|
|
case "page":
|
|
guard let raw = values["limit"], let limit = Int(raw), (1...100).contains(limit) else { return "invalid:invalid_limit" }
|
|
return "valid"
|
|
case "manifest":
|
|
for key in ["provider_url", "vm_address", "password", "private_key"] where values[key] != nil { return "invalid:forbidden_field" }
|
|
return values["version"] == "1" && values["gateway_id"] != nil && (values["grant"]?.utf8.count ?? 0) >= 43 && values["purpose"] == "launch" ? "valid" : "invalid:invalid_manifest"
|
|
case "clipboard": return values["encoding"] == "utf-8" && values["file"] == nil ? "valid" : "invalid:unsupported_clipboard"
|
|
case "event":
|
|
guard values["version"] == "1" else { return "invalid:unsupported_version" }
|
|
if let after = Int(values["after"] ?? ""), let earliest = Int(values["earliest"] ?? ""), after > 0, earliest > 0, after < earliest - 1 { return "invalid:gap" }
|
|
if (Int(values["payload_bytes"] ?? "") ?? Int.max) > 16384 { return "invalid:payload_limit" }
|
|
guard let sequence = Int(values["sequence"] ?? ""), sequence > 0, values["correlation_id"] != nil else { return "invalid:required" }
|
|
return "valid"
|
|
case "tunnel":
|
|
if ["1", "0", "-1"].contains(values["offered"] ?? "") && values["feature"] == "control.v1" { return "valid" }
|
|
return values["feature"] == "control.v1" ? "invalid:unsupported_version" : "invalid:unsupported_feature"
|
|
case "datagram": return classifyDatagram(values["hex"] ?? "")
|
|
case "gateway_input": return classifyGatewayInput(values["hex"] ?? "")
|
|
case "gateway_feedback": return classifyGatewayFeedback(values["hex"] ?? "")
|
|
case "gateway_clipboard":
|
|
if values["file"] != nil { return "invalid:forbidden" }
|
|
guard let direction = values["direction"], let text = values["text"],
|
|
let encoding = values["encoding"], let token = values["loop_token"],
|
|
(try? GatewayClipboardText(
|
|
direction: direction, text: text, encoding: encoding, loopToken: token
|
|
)) != nil else { return "invalid:clipboard" }
|
|
return "valid"
|
|
case "gateway_clipboard_audit":
|
|
if values["text"] != nil { return "invalid:forbidden" }
|
|
guard ["client_to_provider", "provider_to_client"].contains(values["direction"] ?? ""), ["forwarded", "suppressed", "rejected"].contains(values["outcome"] ?? ""), ["forwarded", "loop", "policy", "rate", "provider", "malformed"].contains(values["reason"] ?? ""), let textBytes = Int(values["text_bytes"] ?? ""), (0...65536).contains(textBytes) else { return "invalid:clipboard_audit" }
|
|
return "valid"
|
|
default: return "invalid:unknown_kind"
|
|
}
|
|
}
|
|
|
|
func decodeHex(_ encoded: String) -> [UInt8]? {
|
|
let characters = Array(encoded)
|
|
guard characters.count % 2 == 0 else { return nil }
|
|
var raw: [UInt8] = []
|
|
for index in stride(from: 0, to: characters.count, by: 2) {
|
|
guard let byte = UInt8(String(characters[index...index + 1]), radix: 16) else { return nil }
|
|
raw.append(byte)
|
|
}
|
|
return raw
|
|
}
|
|
|
|
func classifyGatewayInput(_ encoded: String) -> String {
|
|
guard let raw = decodeHex(encoded) else { return "invalid:hex" }
|
|
guard raw.count >= 6 else { return "invalid:truncated" }
|
|
guard Array(raw[0..<4]) == Array("VGI1".utf8) else { return "invalid:magic" }
|
|
let kind = raw[4]
|
|
let body = Array(raw.dropFirst(6))
|
|
guard body.count == Int(raw[5]) else { return "invalid:length" }
|
|
switch kind {
|
|
case 1:
|
|
return body.count == 4 && body[0] <= 1 && (body[2] != 0 || body[3] != 0) ? "valid" : "invalid:field"
|
|
case 2:
|
|
guard body.count == 3 else { return "invalid:length" }
|
|
guard body[0] <= 1 && (1...5).contains(body[1]) else { return "invalid:field" }
|
|
return body[2] == 0 ? "valid" : "invalid:reserved"
|
|
case 3: return body.count == 4 ? "valid" : "invalid:length"
|
|
case 4:
|
|
guard (1...4).contains(body.count), let scalar = String(bytes: body, encoding: .utf8), scalar.unicodeScalars.count == 1 else { return "invalid:utf8" }
|
|
return "valid"
|
|
case 5:
|
|
guard body.count == 17 else { return "invalid:length" }
|
|
guard body[0] <= 15 else { return "invalid:field" }
|
|
guard body[1] != 0 || body[2] != 0 || body.dropFirst(3).allSatisfy({ $0 == 0 }) else { return "invalid:field" }
|
|
return "valid"
|
|
default: return "invalid:kind"
|
|
}
|
|
}
|
|
|
|
func classifyGatewayFeedback(_ encoded: String) -> String {
|
|
guard let raw = decodeHex(encoded) else { return "invalid:hex" }
|
|
guard raw.count >= 8 else { return "invalid:truncated" }
|
|
guard Array(raw[0..<4]) == Array("VGF1".utf8) else { return "invalid:magic" }
|
|
let direction = raw[4]
|
|
let kind = raw[5]
|
|
let body = Array(raw.dropFirst(8))
|
|
guard body.count == Int(raw[6]) * 256 + Int(raw[7]) else { return "invalid:length" }
|
|
guard direction <= 1 else { return "invalid:direction" }
|
|
if direction == 0 {
|
|
if (0x10...0x12).contains(kind) { return "invalid:direction" }
|
|
switch kind {
|
|
case 1: return body.isEmpty ? "valid" : "invalid:length"
|
|
case 2:
|
|
return validFECStatus(body) ? "valid" : "invalid:field"
|
|
case 3: return body.isEmpty ? "valid" : "invalid:length"
|
|
default: return "invalid:type"
|
|
}
|
|
}
|
|
if kind == 1 || kind == 2 || kind == 3 { return "invalid:direction" }
|
|
switch kind {
|
|
case 0x10: return body.count == 4 ? "valid" : "invalid:length"
|
|
case 0x11:
|
|
guard body.count == 5 else { return "invalid:length" }
|
|
return body[0] <= 15 ? "valid" : "invalid:field"
|
|
case 0x12:
|
|
guard body.count == 1 else { return "invalid:length" }
|
|
return body[0] <= 1 ? "valid" : "invalid:field"
|
|
default: return "invalid:type"
|
|
}
|
|
}
|
|
|
|
func validFECStatus(_ body: [UInt8]) -> Bool {
|
|
guard body.count == 21 else { return false }
|
|
let totalData = Int(body[10]) * 256 + Int(body[11])
|
|
let totalParity = Int(body[12]) * 256 + Int(body[13])
|
|
let receivedData = Int(body[14]) * 256 + Int(body[15])
|
|
let receivedParity = Int(body[16]) * 256 + Int(body[17])
|
|
return totalData > 0 && receivedData <= totalData && receivedParity <= totalParity && body[18] <= 100 && body[20] > 0 && body[19] < body[20]
|
|
}
|
|
|
|
func classifyDatagram(_ encoded: String) -> String {
|
|
guard let raw = decodeHex(encoded) else { return "invalid:hex" }
|
|
guard raw.count >= 3 else { return "invalid:truncated" }
|
|
guard raw[0] == 0x56 && raw[1] == 0x44 else { return "invalid:magic" }
|
|
guard raw[2] == 1 || raw[2] == 2 else { return "invalid:unsupported_version" }
|
|
let headerBytes = raw[2] == 1 ? 21 : 23
|
|
guard raw.count >= headerBytes else { return "invalid:truncated" }
|
|
let limit: Int
|
|
switch (raw[2], raw[3]) {
|
|
case (1, 1): limit = 1024
|
|
case (1, 2): limit = 2048
|
|
case (1, 3): limit = 65515
|
|
case (1, 10), (1, 11), (1, 12): limit = 1179
|
|
case (2, 10), (2, 11): limit = 1177
|
|
default: return "invalid:unknown_channel"
|
|
}
|
|
guard raw[4] == 0 else { return "invalid:flags" }
|
|
let fragmentIndex = raw[2] == 1 ? Int(raw[17]) : Int(raw[17]) * 256 + Int(raw[18])
|
|
let fragmentCount = raw[2] == 1 ? Int(raw[18]) : Int(raw[19]) * 256 + Int(raw[20])
|
|
if raw[2] == 2 && fragmentCount > 891 { return "invalid:fragment_limit" }
|
|
guard fragmentCount > 0 && fragmentIndex < fragmentCount else { return "invalid:fragment" }
|
|
let payloadOffset = raw[2] == 1 ? 19 : 21
|
|
let payloadLength = Int(raw[payloadOffset]) * 256 + Int(raw[payloadOffset + 1])
|
|
guard payloadLength <= limit else { return "invalid:payload_limit" }
|
|
guard raw.count == headerBytes + payloadLength else { return "invalid:length_mismatch" }
|
|
guard raw[2] == 1 ? raw.count <= 65536 : raw.count <= 1200 else { return "invalid:frame_limit" }
|
|
return "valid"
|
|
}
|
|
|
|
func normalizedDigest(_ results: [String]) -> String {
|
|
var value: UInt64 = 14695981039346656037
|
|
for result in results {
|
|
for byte in Array("\(result)\n".utf8) {
|
|
value ^= UInt64(byte)
|
|
value = value &* 1099511628211
|
|
}
|
|
}
|
|
return String(format: "%016llx", value)
|
|
}
|
|
|
|
func fixtureHash() -> String {
|
|
let text = try! String(contentsOfFile: "fixtures/manifest.json", encoding: .utf8)
|
|
let marker = "\"corpus_sha256\": \""
|
|
guard let start = text.range(of: marker)?.upperBound else { fatalError("fixture hash") }
|
|
let suffix = text[start...]
|
|
guard let end = suffix.firstIndex(of: "\"") else { fatalError("fixture hash") }
|
|
return String(suffix[..<end])
|
|
}
|
|
|
|
@main
|
|
struct ConformanceMain {
|
|
static func main() {
|
|
let names = try! FileManager.default.contentsOfDirectory(atPath: "fixtures/conformance").filter { $0.hasSuffix(".tsv") }.sorted()
|
|
var results: [String] = []
|
|
for name in names {
|
|
let text = try! String(contentsOfFile: "fixtures/conformance/\(name)", encoding: .utf8)
|
|
var lines = text.split(whereSeparator: \.isNewline).map(String.init)
|
|
precondition(lines.removeFirst() == "id\tversion\tkind\tinput\texpected")
|
|
for line in lines {
|
|
let fields = line.split(separator: "\t", omittingEmptySubsequences: false).map(String.init)
|
|
precondition(fields.count == 5)
|
|
let actual = evaluate(fields[2], fields[3])
|
|
precondition(actual == fields[4], fields[0])
|
|
results.append("\(fields[0])\t\(actual)")
|
|
}
|
|
}
|
|
print("Swift conformance passed normalized=\(normalizedDigest(results)) fixtures=\(fixtureHash())")
|
|
}
|
|
}
|