Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 23 additions & 15 deletions Sources/XcodeProj/Extensions/String+md5.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,34 @@ import Foundation

extension String {
var md5: String {
guard let data = data(using: .utf8, allowLossyConversion: true) else {
return self
}
#if canImport(CryptoKit)
if #available(OSX 10.15, iOS 13.0, *) {
return Insecure.MD5.hash(data: data)
.withUnsafeBytes { Array($0) }.hexString
} else {
return data.slowMD5
// Hashes the UTF-8 bytes in place and writes the hex digits straight into the result, without the
// intermediate `Data`, byte array, and hex buffer.
var string = self
let digest = string.withUTF8 { Insecure.MD5.hash(data: UnsafeRawBufferPointer($0)) }
return digest.withUnsafeBytes { bytes in
String(unsafeUninitializedCapacity: bytes.count * 2) { hex in
for (offset, byte) in bytes.enumerated() {
hex[offset * 2] = hexDigit(byte >> 4)
hex[offset * 2 + 1] = hexDigit(byte & 0xF)
}
return bytes.count * 2
}
}
}
#else
return data.slowMD5
#endif
guard let data = data(using: .utf8, allowLossyConversion: true) else {
return self
}
return data.slowMD5
}
}

private func hexDigit(_ value: UInt8) -> UInt8 {
(value > 9) ? (charA + value - 10) : (char0 + value)
}

private let charA = UInt8(UnicodeScalar("a").value)
private let char0 = UInt8(UnicodeScalar("0").value)

Expand All @@ -54,18 +66,14 @@ private extension DataProtocol {

for _ in regions {
for i in self {
hexChars[Int(offset * 2)] = itoh((i >> 4) & 0xF)
hexChars[Int(offset * 2 + 1)] = itoh(i & 0xF)
hexChars[Int(offset * 2)] = hexDigit((i >> 4) & 0xF)
hexChars[Int(offset * 2 + 1)] = hexDigit(i & 0xF)
offset += 1
}
}

return String(bytes: hexChars, encoding: .utf8)!
}

func itoh(_ value: UInt8) -> UInt8 {
(value > 9) ? (charA + value - 10) : (char0 + value)
}
}

private extension Data {
Expand Down
3 changes: 2 additions & 1 deletion Sources/XcodeProj/Objects/BuildPhase/PBXBuildFile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,8 @@ final class PBXBuildPhaseFile: PlistSerializable, Equatable {
}

func plistKeyAndValue(proj _: PBXProj, reference: String) throws -> (key: CommentedString, value: PlistValue) {
var dictionary: [CommentedString: PlistValue] = [:]
// Room for every key below, so the dictionary isn't regrown while it's filled.
var dictionary = [CommentedString: PlistValue](minimumCapacity: 6)
dictionary["isa"] = .string(CommentedString(PBXBuildFile.isa))
if let fileReference = buildFile.fileReference {
let fileElement: PBXFileElement? = fileReference.getObject()
Expand Down
3 changes: 2 additions & 1 deletion Sources/XcodeProj/Objects/Project/PBXObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ public class PBXObject: Hashable, Decodable, Equatable, AutoEquatable {

/// Object isa (type id)
public static var isa: String {
String(describing: self)
// What `String(describing: self)` prints for a class, without the reflection it goes through first.
_typeName(self, qualified: false)
}

public static func == (lhs: PBXObject,
Expand Down
16 changes: 13 additions & 3 deletions Sources/XcodeProj/Objects/Project/PBXObjectReference.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ class PBXObjectReference: NSObject, Comparable, NSCopying {
private(set) var temporary: Bool

/// String reference.
private(set) var value: String
private(set) var value: String {
didSet { valueHash = value.hashValue }
}

/// The hash of `value`, which dictionaries keyed by references ask for again every time they move an entry.
private var valueHash: Int

/// Weak reference to the objects instance that contains the project objects.
weak var objects: PBXObjects?
Expand All @@ -20,13 +25,15 @@ class PBXObjectReference: NSObject, Comparable, NSCopying {
/// - Parameter reference: reference.
init(_ reference: String, objects: PBXObjects) {
value = reference
valueHash = reference.hashValue
temporary = false
self.objects = objects
}

/// Initializes a temporary reference
init(objects: PBXObjects? = nil) {
value = "TEMP_\(UUID().uuidString)"
valueHash = value.hashValue
temporary = true
self.objects = objects
}
Expand All @@ -36,6 +43,7 @@ class PBXObjectReference: NSObject, Comparable, NSCopying {
/// - Parameter reference: reference.
init(_ reference: String) {
value = reference
valueHash = reference.hashValue
temporary = false
}

Expand All @@ -44,14 +52,16 @@ class PBXObjectReference: NSObject, Comparable, NSCopying {
/// - Parameter objectReference: object reference to be initialized from.
required init(_ objectReference: PBXObjectReference) {
value = objectReference.value
valueHash = objectReference.valueHash
temporary = objectReference.temporary
}

/// Fixes its value making it permanent.
///
/// - Parameter value: value.
func fix(_ value: String) {
let object = objects?.delete(reference: self)
// The referenced object's type says which collection holds it, which saves probing every collection.
let object = object.flatMap { objects?.remove(reference: self, typeOf: $0) } ?? objects?.delete(reference: self)
self.value = value
temporary = false
if let object {
Expand All @@ -71,7 +81,7 @@ class PBXObjectReference: NSObject, Comparable, NSCopying {

/// Hash value.
override var hash: Int {
value.hashValue
valueHash
}

func copy(with _: NSZone? = nil) -> Any {
Expand Down
106 changes: 67 additions & 39 deletions Sources/XcodeProj/Objects/Project/PBXObjects.swift
Original file line number Diff line number Diff line change
Expand Up @@ -208,40 +208,70 @@ class PBXObjects: Equatable {
}
let objectReference: PBXObjectReference = object.reference
objectReference.objects = self
update(object, reference: objectReference, inserting: true)
}

/// Removes the object stored under `reference` from the collection that objects of `object`'s type belong to.
/// Unlike `delete(reference:)`, it doesn't probe every collection.
///
/// - Returns: the removed object, or nil when that collection has nothing under `reference`.
func remove(reference: PBXObjectReference, typeOf object: PBXObject) -> PBXObject? {
lock.lock()
defer { lock.unlock() }
return update(object, reference: reference, inserting: false)
}

/// Inserts `object` under `reference` into the collection its type belongs to, or removes what that collection
/// stores under `reference`, so `add` and `remove` always agree on the collection.
@discardableResult private func update(
_ object: PBXObject,
reference: PBXObjectReference,
inserting: Bool
) -> PBXObject? {
func update<T: PBXObject>(_ objects: inout [PBXObjectReference: T], _ object: T) -> PBXObject? {
if inserting {
objects[reference] = object
return object
}
return objects.removeValue(forKey: reference)
}

switch object {
// subclasses of PBXGroup; must be tested before PBXGroup
case let object as PBXVariantGroup: _variantGroups[objectReference] = object
case let object as XCVersionGroup: _versionGroups[objectReference] = object
case let object as PBXVariantGroup: return update(&_variantGroups, object)
case let object as XCVersionGroup: return update(&_versionGroups, object)
// everything else
case let object as PBXBuildFile: _buildFiles[objectReference] = object
case let object as PBXAggregateTarget: _aggregateTargets[objectReference] = object
case let object as PBXLegacyTarget: _legacyTargets[objectReference] = object
case let object as PBXContainerItemProxy: _containerItemProxies[objectReference] = object
case let object as PBXCopyFilesBuildPhase: _copyFilesBuildPhases[objectReference] = object
case let object as PBXGroup: _groups[objectReference] = object
case let object as XCConfigurationList: _configurationLists[objectReference] = object
case let object as XCBuildConfiguration: _buildConfigurations[objectReference] = object
case let object as PBXTargetDependency: _targetDependencies[objectReference] = object
case let object as PBXSourcesBuildPhase: _sourcesBuildPhases[objectReference] = object
case let object as PBXShellScriptBuildPhase: _shellScriptBuildPhases[objectReference] = object
case let object as PBXResourcesBuildPhase: _resourcesBuildPhases[objectReference] = object
case let object as PBXFrameworksBuildPhase: _frameworksBuildPhases[objectReference] = object
case let object as PBXHeadersBuildPhase: _headersBuildPhases[objectReference] = object
case let object as PBXNativeTarget: _nativeTargets[objectReference] = object
case let object as PBXFileReference: _fileReferences[objectReference] = object
case let object as PBXProject: _projects[objectReference] = object
case let object as PBXReferenceProxy: _referenceProxies[objectReference] = object
case let object as PBXRezBuildPhase: _carbonResourcesBuildPhases[objectReference] = object
case let object as PBXBuildRule: _buildRules[objectReference] = object
case let object as XCRemoteSwiftPackageReference: _remoteSwiftPackageReferences[objectReference] = object
case let object as XCLocalSwiftPackageReference: _localSwiftPackageReferences[objectReference] = object
case let object as XCSwiftPackageProductDependency: _swiftPackageProductDependencies[objectReference] = object
case let object as PBXFileSystemSynchronizedRootGroup: _fileSystemSynchronizedRootGroups[objectReference] = object
case let object as PBXFileSystemSynchronizedBuildFileExceptionSet: _fileSystemSynchronizedBuildFileExceptionSets[objectReference] = object
case let object as PBXBuildFile: return update(&_buildFiles, object)
case let object as PBXAggregateTarget: return update(&_aggregateTargets, object)
case let object as PBXLegacyTarget: return update(&_legacyTargets, object)
case let object as PBXContainerItemProxy: return update(&_containerItemProxies, object)
case let object as PBXCopyFilesBuildPhase: return update(&_copyFilesBuildPhases, object)
case let object as PBXGroup: return update(&_groups, object)
case let object as XCConfigurationList: return update(&_configurationLists, object)
case let object as XCBuildConfiguration: return update(&_buildConfigurations, object)
case let object as PBXTargetDependency: return update(&_targetDependencies, object)
case let object as PBXSourcesBuildPhase: return update(&_sourcesBuildPhases, object)
case let object as PBXShellScriptBuildPhase: return update(&_shellScriptBuildPhases, object)
case let object as PBXResourcesBuildPhase: return update(&_resourcesBuildPhases, object)
case let object as PBXFrameworksBuildPhase: return update(&_frameworksBuildPhases, object)
case let object as PBXHeadersBuildPhase: return update(&_headersBuildPhases, object)
case let object as PBXNativeTarget: return update(&_nativeTargets, object)
case let object as PBXFileReference: return update(&_fileReferences, object)
case let object as PBXProject: return update(&_projects, object)
case let object as PBXReferenceProxy: return update(&_referenceProxies, object)
case let object as PBXRezBuildPhase: return update(&_carbonResourcesBuildPhases, object)
case let object as PBXBuildRule: return update(&_buildRules, object)
case let object as XCRemoteSwiftPackageReference: return update(&_remoteSwiftPackageReferences, object)
case let object as XCLocalSwiftPackageReference: return update(&_localSwiftPackageReferences, object)
case let object as XCSwiftPackageProductDependency: return update(&_swiftPackageProductDependencies, object)
case let object as PBXFileSystemSynchronizedRootGroup: return update(&_fileSystemSynchronizedRootGroups, object)
case let object as PBXFileSystemSynchronizedBuildFileExceptionSet: return update(&_fileSystemSynchronizedBuildFileExceptionSets, object)
case let object as PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet:
_fileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet[objectReference] = object
default: fatalError("Unhandled PBXObject type for \(object), this is likely a bug / todo")
return update(&_fileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet, object)
default:
// Objects of these types are never added, so there is nothing to remove.
guard inserting else { return nil }
fatalError("Unhandled PBXObject type for \(object), this is likely a bug / todo")
}
}

Expand Down Expand Up @@ -430,17 +460,15 @@ extension PBXObjects {
// This is used to decode build files. (we need the name of the build phase)
// Otherwise, we would have to go through all the build phases for each file.
var buildPhaseFile: [PBXObjectReference: PBXBuildPhaseFile] {
let values: [[PBXBuildPhaseFile]] = buildPhases.values.map { buildPhase in
let files = buildPhase.files
let buildPhaseFile: [PBXBuildPhaseFile] = files?.compactMap { (file: PBXBuildFile) -> PBXBuildPhaseFile in
PBXBuildPhaseFile(
buildFile: file,
buildPhase: buildPhase
)
} ?? []
return buildPhaseFile
// A build file listed by several phases maps to the first of them. Visiting the phases and files backwards lets
// each entry be overwritten without checking it first, so the first occurrence is the one that stays.
var buildPhaseFile = [PBXObjectReference: PBXBuildPhaseFile](minimumCapacity: buildFiles.count)
for buildPhase in buildPhases.values.reversed() {
for file in (buildPhase.files ?? []).reversed() {
buildPhaseFile[file.reference] = PBXBuildPhaseFile(buildFile: file, buildPhase: buildPhase)
}
}
return Dictionary(values.flatMap { $0 }.map { ($0.buildFile.reference, $0) }, uniquingKeysWith: { first, _ in first })
return buildPhaseFile
}

/// Runs the given closure for each of the objects that are part of the project.
Expand Down
6 changes: 4 additions & 2 deletions Sources/XcodeProj/Objects/Project/PBXProjEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -374,9 +374,11 @@ final class PBXProjEncoder {
}
}

let joinedArray: [String] = resultArray.flatMap { $0 }
// should check multiline
output.append(contentsOf: joinedArray)
// Appended element by element, which saves flattening them into a temporary array first.
for lines in resultArray {
output.append(contentsOf: lines)
}
write(string: "/* End \(section) section */", to: &output)
writeNewLine(stateHolder: &stateHolder, to: &output)
}
Expand Down
26 changes: 18 additions & 8 deletions Sources/XcodeProj/Utils/ReferenceGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ protocol ReferenceGenerating: AnyObject {
final class ReferenceGenerator: ReferenceGenerating {
let outputSettings: PBXOutputSettings
var references: Set<String> = []
/// The type name and reference acronym of each object type, which only depend on the type.
private var typeNamesAndAcronyms: [ObjectIdentifier: (typeName: String, acronym: String)] = [:]

init(outputSettings: PBXOutputSettings) {
self.outputSettings = outputSettings
Expand Down Expand Up @@ -386,14 +388,7 @@ extension ReferenceGenerator {
if let context = object.context {
identifiers.append(context)
}
let typeName = String(describing: type(of: object))

// Get acronym to be used as prefix for the reference.
// PBXFileReference is turned to FR.
let acronym = typeName
.replacingOccurrences(of: "PBX", with: "")
.replacingOccurrences(of: "XC", with: "")
.filter { String($0).lowercased() != String($0) }
let (typeName, acronym) = typeNameAndAcronym(of: object)

var reference = ""
var counter = 0
Expand All @@ -410,6 +405,21 @@ extension ReferenceGenerator {
}
}

private func typeNameAndAcronym(of object: some PBXObject) -> (typeName: String, acronym: String) {
let objectType = type(of: object)
if let cached = typeNamesAndAcronyms[ObjectIdentifier(objectType)] { return cached }
let typeName = String(describing: objectType)

// Get acronym to be used as prefix for the reference.
// PBXFileReference is turned to FR.
let acronym = typeName
.replacingOccurrences(of: "PBX", with: "")
.replacingOccurrences(of: "XC", with: "")
.filter { String($0).lowercased() != String($0) }
typeNamesAndAcronyms[ObjectIdentifier(objectType)] = (typeName, acronym)
return (typeName, acronym)
}

private func generateReferenceFrom(acronym: String,
typeName: String,
identifiers: [String],
Expand Down
Loading
Loading