138 lines
6.7 KiB
Swift
138 lines
6.7 KiB
Swift
import AppKit
|
|
|
|
let output = URL(fileURLWithPath: CommandLine.arguments[1], isDirectory: true)
|
|
let root = NSView(frame: NSRect(x: 0, y: 0, width: 80, height: 40))
|
|
root.identifier = NSUserInterfaceItemIdentifier("Café")
|
|
for name in ["Left", "Right"] {
|
|
let child = NSView(frame: .zero)
|
|
child.identifier = NSUserInterfaceItemIdentifier(name)
|
|
root.addSubview(child)
|
|
}
|
|
let archiver = NSKeyedArchiver(requiringSecureCoding: false)
|
|
archiver.outputFormat = .xml
|
|
archiver.encode(root, forKey: NSKeyedArchiveRootObjectKey)
|
|
archiver.finishEncoding()
|
|
let original = archiver.encodedData
|
|
let text = String(data: original, encoding: .utf8)!
|
|
let reference = try PropertyListSerialization.propertyList(from: original, options: [], format: nil)
|
|
let referenceXML = try PropertyListSerialization.data(fromPropertyList: reference, format: .xml, options: 0)
|
|
let utf16Text = text.replacingOccurrences(of: "UTF-8", with: "UTF-16")
|
|
let representations: [String: Data] = [
|
|
"utf8": original,
|
|
"utf8-bom": Data([0xef, 0xbb, 0xbf]) + original,
|
|
"utf16le": Data([0xff, 0xfe]) + utf16Text.data(using: .utf16LittleEndian)!,
|
|
"utf16be": Data([0xfe, 0xff]) + utf16Text.data(using: .utf16BigEndian)!,
|
|
"binary": try PropertyListSerialization.data(fromPropertyList: reference, format: .binary, options: 0),
|
|
]
|
|
for (name, bytes) in representations {
|
|
let decoded = try PropertyListSerialization.propertyList(from: bytes, options: [], format: nil)
|
|
let decodedXML = try PropertyListSerialization.data(fromPropertyList: decoded, format: .xml, options: 0)
|
|
guard decodedXML == referenceXML else {
|
|
fatalError("Foundation rejected graph parity for \(name)")
|
|
}
|
|
try bytes.write(to: output.appendingPathComponent("\(name).nib"))
|
|
}
|
|
|
|
// Foundation materializes opaque UID objects. Read original XML markers separately.
|
|
final class PlistElement {
|
|
let kind: String
|
|
var text = ""
|
|
var children: [PlistElement] = []
|
|
init(_ kind: String) { self.kind = kind }
|
|
}
|
|
final class PlistXMLReader: NSObject, XMLParserDelegate {
|
|
let document = PlistElement("document")
|
|
var stack: [PlistElement] = []
|
|
override init() { super.init(); stack = [document] }
|
|
func parser(_ parser: XMLParser, didStartElement name: String, namespaceURI: String?, qualifiedName: String?, attributes: [String: String]) {
|
|
let element = PlistElement(name)
|
|
stack.last!.children.append(element)
|
|
stack.append(element)
|
|
}
|
|
func parser(_ parser: XMLParser, foundCharacters text: String) { stack.last!.text += text }
|
|
func parser(_ parser: XMLParser, didEndElement name: String, namespaceURI: String?, qualifiedName: String?) { stack.removeLast() }
|
|
}
|
|
func readPlist(_ element: PlistElement) -> Any {
|
|
switch element.kind {
|
|
case "plist": return readPlist(element.children[0])
|
|
case "dict":
|
|
guard element.children.count % 2 == 0 else { fatalError("Incomplete generated dictionary") }
|
|
var result: [String: Any] = [:]
|
|
for index in stride(from: 0, to: element.children.count, by: 2) {
|
|
guard element.children[index].kind == "key" else { fatalError("Missing generated dictionary key") }
|
|
result[element.children[index].text] = readPlist(element.children[index + 1])
|
|
}
|
|
return result
|
|
case "array": return element.children.map(readPlist)
|
|
case "string": return element.text
|
|
case "integer": return Int(element.text)!
|
|
case "true": return true
|
|
case "false": return false
|
|
default: fatalError("Unsupported generated oracle element \(element.kind)")
|
|
}
|
|
}
|
|
let xmlReader = PlistXMLReader()
|
|
let xmlParser = XMLParser(data: original)
|
|
xmlParser.shouldResolveExternalEntities = false
|
|
xmlParser.delegate = xmlReader
|
|
guard xmlParser.parse(), let plist = xmlReader.document.children.first else {
|
|
fatalError("Cannot read generated XML object table")
|
|
}
|
|
let originalXMLTable = readPlist(plist)
|
|
|
|
// Extract actual UID/class/identifier/parent facts, without REA or unarchiving.
|
|
guard let archive = originalXMLTable as? [String: Any],
|
|
let table = archive["$objects"] as? [Any],
|
|
let top = archive["$top"] as? [String: Any] else {
|
|
fatalError("Expected a Foundation object table")
|
|
}
|
|
func uid(_ value: Any?) -> Int {
|
|
guard let marker = value as? [String: Any], marker.count == 1,
|
|
let number = marker["CF$UID"] as? NSNumber else {
|
|
fatalError("Expected Foundation XML UID marker")
|
|
}
|
|
return number.intValue
|
|
}
|
|
let rootUID = uid(top[NSKeyedArchiveRootObjectKey])
|
|
guard let rootFields = table[rootUID] as? [String: Any],
|
|
let collection = table[uid(rootFields["NSSubviews"])] as? [String: Any],
|
|
let childReferences = collection["NS.objects"] as? [Any] else {
|
|
fatalError("Expected serialized NSView subviews")
|
|
}
|
|
let childUIDs = childReferences.map { uid($0) }
|
|
var views: [[String: Any]] = []
|
|
for index in [rootUID] + childUIDs {
|
|
guard let fields = table[index] as? [String: Any],
|
|
let descriptor = table[uid(fields["$class"])] as? [String: Any],
|
|
descriptor["$classname"] as? String == "NSView",
|
|
let identifier = table[uid(fields["NSReuseIdentifierKey"])] as? String else {
|
|
fatalError("Expected NSView class and serialized identifier")
|
|
}
|
|
if index != rootUID && uid(fields["NSSuperview"]) != rootUID {
|
|
fatalError("Foundation did not retain the expected parent link")
|
|
}
|
|
views.append(["uid": index, "identifier": identifier])
|
|
}
|
|
guard views.compactMap({ $0["identifier"] as? String }) == ["Café", "Left", "Right"] else {
|
|
fatalError("Foundation did not retain the authored identifiers")
|
|
}
|
|
let oracle: [String: Any] = ["rootUID": rootUID, "childUIDs": childUIDs, "views": views]
|
|
try JSONSerialization.data(withJSONObject: oracle, options: [.sortedKeys])
|
|
.write(to: output.appendingPathComponent("oracle.json"))
|
|
|
|
// Deliberately invalid controls are separate from the native-validated representations.
|
|
try Data([0xff, 0xfe, 0x3c]).write(to: output.appendingPathComponent("incomplete-utf16.nib"))
|
|
try Data(utf16Text.utf8).write(to: output.appendingPathComponent("utf8-declared-utf16.nib"))
|
|
try (Data([0xff, 0xfe]) + text.data(using: .utf16LittleEndian)!).write(to: output.appendingPathComponent("utf16-declared-utf8.nib"))
|
|
try Data(text.replacingOccurrences(of: "UTF-8", with: "REA-UNSUPPORTED").utf8)
|
|
.write(to: output.appendingPathComponent("unsupported-declaration.nib"))
|
|
try Data(text.replacingOccurrences(of: "UTF-8", with: "ISO-8859-1").utf8)
|
|
.write(to: output.appendingPathComponent("latin1-declaration.nib"))
|
|
|
|
guard let identifierRange = original.range(of: Data("Café".utf8)) else {
|
|
fatalError("Expected literal UTF-8 identifier in generated XML")
|
|
}
|
|
var invalidUTF8 = original
|
|
invalidUTF8[identifierRange.lowerBound] = 0xff
|
|
try invalidUTF8.write(to: output.appendingPathComponent("invalid-utf8.nib"))
|