| // Copyright 2016 The Tulsi Authors. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| import Foundation |
| |
| let XcodeProjectArchiveVersion = "1" |
| |
| |
| protocol PBXProjSerializable: class { |
| func serializeInto(_ serializer: PBXProjFieldSerializer) throws |
| } |
| |
| |
| /// Methods for serializing components of a PBXObject. |
| protocol PBXProjFieldSerializer { |
| func addField(_ name: String, _ obj: PBXObjectProtocol?, rawID: Bool) throws |
| func addField(_ name: String, _ val: Int) throws |
| func addField(_ name: String, _ val: Bool) throws |
| func addField(_ name: String, _ val: String?) throws |
| func addField(_ name: String, _ val: [String: Any]?) throws |
| func addField<T: PBXObjectProtocol>(_ name: String, _ values: [T]) throws |
| func addField(_ name: String, _ values: [String]) throws |
| |
| // Serializes an object if it has not already been serialized and returns its globalID, optionally |
| // with a comment string attached unless returnRawID is true. |
| func serializeObject(_ object: PBXObjectProtocol, returnRawID: Bool) throws -> String |
| } |
| |
| extension PBXProjFieldSerializer { |
| func addField(_ name: String, _ obj: PBXObjectProtocol?) throws { |
| try addField(name, obj, rawID: false) |
| } |
| } |
| |
| |
| extension NSMutableData { |
| enum EncodingError: Error { |
| // A string failed to be encoded into NSData as UTF8. |
| case stringUTF8EncodingError |
| } |
| |
| func tulsi_appendString(_ str: String) throws { |
| guard let encoded = str.data(using: String.Encoding.utf8) else { |
| throw EncodingError.stringUTF8EncodingError |
| } |
| self.append(encoded) |
| } |
| } |
| |
| |
| /// Encapsulates the ability to serialize a PBXProject into an OpenStep formatted plist. |
| final class OpenStepSerializer: PBXProjFieldSerializer { |
| |
| private enum SerializationError: Error { |
| // A PBX object was referenced but never defined. |
| case referencedObjectNotFoundError |
| } |
| |
| // List of objects that are always serialized on a single line by Xcode. |
| private static let CompactPBXTypes = Set<String>(["PBXBuildFile", "PBXFileReference"]) |
| |
| private let rootObject: PBXProject |
| private let gidGenerator: GIDGeneratorProtocol |
| private var objects = [String: TypedDict]() |
| |
| private class GIDHolder { |
| var gids: [String] = [] |
| } |
| // Maps PBXObject types to arrays of keys in the objects array. This allows serialization in the |
| // same OpenStep format as Xcode. A wrapper GIDHolder class is leveraged in place of directly |
| // storing arrays of keys to avoid performance issues due to value semantics as the arrays are |
| // mutated. |
| private var typedObjectIndex = [String: GIDHolder]() |
| |
| // Dictionary containing data for the object currently being serialized. |
| private var currentDict: RawDict! |
| |
| init(rootObject: PBXProject, gidGenerator: GIDGeneratorProtocol) { |
| self.rootObject = rootObject |
| self.gidGenerator = gidGenerator |
| } |
| |
| func serialize() -> Data? { |
| do { |
| let rootObjectID = try serializeObject(rootObject) |
| return serializeObjectDictionaryWithRoot(rootObjectID) |
| } catch { |
| return nil |
| } |
| } |
| |
| // MARK: - XcodeProjFieldSerializer |
| func serializeObject(_ obj: PBXObjectProtocol, returnRawID: Bool = false) throws -> String { |
| if let typedObject = objects[obj.globalID] { |
| if !returnRawID { |
| return "\(obj.globalID)\(typedObject.comment)" |
| } |
| return obj.globalID |
| } |
| |
| // If the object doesn't have a GID from a previous serialization, generate one. |
| if obj.globalID.isEmpty { |
| obj.globalID = gidGenerator.generate(obj) |
| } |
| |
| let globalID = obj.globalID |
| let isa = obj.isa |
| let serializationDict = TypedDict(gid: globalID, isa: isa, comment: obj.comment) |
| let stack = currentDict |
| currentDict = serializationDict |
| |
| // Note: The object must be added to the objects dictionary prior to serialization in order to |
| // allow recursive references. e.g., PBXTargetDependency instances between targets in the same |
| // PBXProject instance. |
| objects[globalID] = serializationDict |
| |
| try obj.serializeInto(self) |
| |
| var objectGIDHolder = typedObjectIndex[isa] |
| if objectGIDHolder == nil { |
| objectGIDHolder = GIDHolder() |
| typedObjectIndex[isa] = objectGIDHolder |
| } |
| objectGIDHolder!.gids.append(globalID) |
| |
| currentDict = stack |
| |
| if returnRawID { |
| return globalID |
| } |
| return "\(globalID)\(serializationDict.comment)" |
| } |
| |
| func addField(_ name: String, _ obj: PBXObjectProtocol?, rawID: Bool) throws { |
| guard let local = obj else { |
| return |
| } |
| |
| let gid = try serializeObject(local, returnRawID: rawID) |
| currentDict.dict[name] = gid as AnyObject? |
| } |
| |
| func addField(_ name: String, _ val: Int) throws { |
| currentDict.dict[name] = val as AnyObject? |
| } |
| |
| func addField(_ name: String, _ val: Bool) throws { |
| let intVal = val ? 1 : 0 |
| currentDict.dict[name] = intVal as AnyObject? |
| } |
| |
| func addField(_ name: String, _ val: String?) throws { |
| guard let stringValue = val else { |
| return |
| } |
| |
| currentDict.dict[name] = escapeString(stringValue) as AnyObject? |
| } |
| |
| func addField(_ name: String, _ val: [String: Any]?) throws { |
| // Note: Xcode will crash if empty buildSettings member dictionaries of XCBuildConfiguration's |
| // are omitted so this does not check to see if the dictionary is empty or not. |
| guard let dict = val else { |
| return |
| } |
| |
| let stack = currentDict |
| currentDict = RawDict(compact: (stack?.compact)!) |
| for (key, value) in dict { |
| if let stringValue = value as? String { |
| currentDict.dict[key] = escapeString(stringValue) as AnyObject? |
| } else if let dictValue = value as? [String: AnyObject] { |
| try addField(key, dictValue) |
| } else if let arrayValue = value as? [String] { |
| try addField(key, arrayValue) |
| } else { |
| assertionFailure("Unsupported complex object \(value) in nested dictionary type") |
| } |
| } |
| stack?.dict[name] = currentDict |
| currentDict = stack |
| |
| } |
| |
| func addField<T: PBXObjectProtocol>(_ name: String, _ values: [T]) throws { |
| currentDict.dict[name] = try values.map() { try serializeObject($0) } |
| } |
| |
| func addField(_ name: String, _ values: [String]) throws { |
| currentDict.dict[name] = values.map() { escapeString($0) } |
| } |
| |
| func getGlobalIDForObject(_ object: PBXObjectProtocol) throws -> String { |
| return try serializeObject(object) |
| } |
| |
| // MARK: - Private methods |
| |
| private func serializeObjectDictionaryWithRoot(_ rootObjectID: String) -> Data? { |
| let data = NSMutableData() |
| do { |
| try data.tulsi_appendString("// !$*UTF8*$!\n{\n") |
| var indent = "\t" |
| |
| func appendIndentedString(_ value: String) throws { |
| try data.tulsi_appendString(indent + value) |
| } |
| |
| try appendIndentedString("archiveVersion = \(XcodeProjectArchiveVersion);\n") |
| try appendIndentedString("classes = {\n\(indent)};\n") |
| try appendIndentedString("objectVersion = \(XcodeVersionInfo.objectVersion);\n") |
| |
| try appendIndentedString("objects = {\n") |
| let oldIndent = indent |
| indent += "\t" |
| |
| try encodeSerializedPBXObjectArray("PBXBuildFile", into: data, indented: indent) |
| try encodeSerializedPBXObjectArray("PBXContainerItemProxy", into: data, indented: indent) |
| try encodeSerializedPBXObjectArray("PBXFileReference", into: data, indented: indent) |
| try encodeSerializedPBXObjectArray("PBXGroup", into: data, indented: indent) |
| try encodeSerializedPBXObjectArray("PBXLegacyTarget", into: data, indented: indent) |
| try encodeSerializedPBXObjectArray("PBXNativeTarget", into: data, indented: indent) |
| try encodeSerializedPBXObjectArray("PBXProject", into: data, indented: indent) |
| try encodeSerializedPBXObjectArray("PBXShellScriptBuildPhase", into: data, indented: indent) |
| try encodeSerializedPBXObjectArray("PBXSourcesBuildPhase", into: data, indented: indent) |
| try encodeSerializedPBXObjectArray("PBXTargetDependency", into: data, indented: indent) |
| try encodeSerializedPBXObjectArray("XCBuildConfiguration", into: data, indented: indent) |
| try encodeSerializedPBXObjectArray("XCConfigurationList", into: data, indented: indent) |
| try encodeSerializedPBXObjectArray("XCVersionGroup", into: data, indented: indent) |
| |
| assert(typedObjectIndex.isEmpty, "Failed to encode objects of type(s) \(typedObjectIndex.keys)") |
| |
| indent = oldIndent |
| try appendIndentedString("};\n") |
| try appendIndentedString("rootObject = \(rootObjectID);\n") |
| try data.tulsi_appendString("}") |
| } catch { |
| return nil |
| } |
| |
| return data as Data |
| } |
| |
| private func encodeSerializedPBXObjectArray(_ key: String, |
| into data: NSMutableData, |
| indented indent: String) throws { |
| guard let entries = typedObjectIndex[key]?.gids else { |
| return |
| } |
| |
| // For debugging purposes, throw away the index now that it's being encoded. |
| typedObjectIndex.removeValue(forKey: key) |
| |
| try data.tulsi_appendString("\n/* Begin \(key) section */\n") |
| |
| for gid in entries.sorted() { |
| guard let obj = objects[gid] else { |
| throw SerializationError.referencedObjectNotFoundError |
| } |
| |
| try obj.appendToData(data, indent: indent) |
| } |
| |
| try data.tulsi_appendString("/* End \(key) section */\n") |
| } |
| |
| /// Intermediate representation of a raw dictionary. |
| private class RawDict { |
| var dict = [String: Any]() |
| let compact: Bool |
| |
| init(compact: Bool = false) { |
| self.compact = compact |
| } |
| |
| func appendToData(_ data: NSMutableData, indent: String) throws { |
| try data.tulsi_appendString("{") |
| |
| let (closingSpacer, spacer) = spacersForIndent(indent) |
| if !compact { |
| try data.tulsi_appendString(spacer) |
| } |
| |
| try appendContentsToData(data, indent: indent, spacer: spacer) |
| try data.tulsi_appendString("\(closingSpacer)};") |
| } |
| |
| // MARK: - Internal methods |
| |
| func appendContentsToData(_ data: NSMutableData, indent: String, spacer: String) throws { |
| let newIndent = indent + "\t" |
| var leadingSpacer = "" |
| for (key, value) in dict.sorted(by: { $0.0 < $1.0 }) { |
| let key = escapeString(key) |
| if let rawDictValue = value as? RawDict { |
| try data.tulsi_appendString("\(leadingSpacer)\(key) = ") |
| try rawDictValue.appendToData(data, indent: newIndent) |
| } else if let arrayValue = value as? [String] { |
| try data.tulsi_appendString("\(leadingSpacer)\(key) = (") |
| let itemSpacer = spacer + (compact ? "" : "\t") |
| try arrayValue.forEach() { try data.tulsi_appendString("\(itemSpacer)\($0),") } |
| try data.tulsi_appendString("\(spacer));") |
| } else { |
| try data.tulsi_appendString("\(leadingSpacer)\(key) = \(value);") |
| } |
| leadingSpacer = spacer |
| } |
| } |
| |
| func spacersForIndent(_ indent: String) -> (String, String) { |
| let closingSpacer: String |
| let spacer: String |
| if compact { |
| spacer = " " |
| closingSpacer = spacer |
| } else { |
| closingSpacer = "\n\(indent)" |
| spacer = "\(closingSpacer)\t" |
| } |
| return (closingSpacer, spacer) |
| } |
| } |
| |
| |
| /// Intermediate representation of a typed PBXObject. |
| private final class TypedDict: RawDict { |
| let gid: String |
| let isa: String |
| let comment: String |
| |
| init(gid: String, isa: String, comment: String? = nil) { |
| self.gid = gid |
| self.isa = isa |
| if let comment = comment { |
| self.comment = " /* \(comment) */" |
| } else { |
| self.comment = "" |
| } |
| super.init(compact: OpenStepSerializer.CompactPBXTypes.contains(isa)) |
| } |
| |
| override func appendToData(_ data: NSMutableData, indent: String) throws { |
| try data.tulsi_appendString("\(indent)\(gid)\(comment) = {") |
| |
| let (closingSpacer, spacer) = spacersForIndent(indent) |
| if !compact { |
| try data.tulsi_appendString(spacer) |
| } |
| |
| try data.tulsi_appendString("isa = \(isa);\(spacer)") |
| try appendContentsToData(data, indent: indent, spacer: spacer) |
| try data.tulsi_appendString("\(closingSpacer)};\n") |
| } |
| } |
| } |
| |
| |
| // Regex used to determine whether a string value can be printed without quotes or not. |
| private let unquotedSerializableStringRegex = try! NSRegularExpression(pattern: "^[A-Z0-9._/]+$", |
| options: [.caseInsensitive]) |
| |
| private func escapeString(_ val: String) -> String { |
| var val = val |
| // The quotation marks can be omitted if the string is composed strictly of alphanumeric |
| // characters and contains no white space (numbers are handled as strings in property lists). |
| // Though the property list format uses ASCII for strings, note that Cocoa uses Unicode. Since |
| // string encodings vary from region to region, this representation makes the format fragile. |
| // You may see strings containing unreadable sequences of ASCII characters; these are used to |
| // represent Unicode characters. |
| let valueRange = NSMakeRange(0, val.count) |
| if unquotedSerializableStringRegex.firstMatch(in: val, options: NSRegularExpression.MatchingOptions.anchored, range: valueRange) != nil { |
| return val |
| } else { |
| val = val.replacingOccurrences(of: "\\", with: "\\\\") |
| val = val.replacingOccurrences(of: "\"", with: "\\\"") |
| val = val.replacingOccurrences(of: "\n", with: "\\n") |
| return "\"\(val)\"" |
| } |
| } |