From c3f69fce075037848ced8e3f6110b06d8303adb2 Mon Sep 17 00:00:00 2001 From: "Md. Ibrahim Hassan" Date: Thu, 12 Oct 2023 14:11:33 +0530 Subject: [PATCH] Create `PBXProj` class from the Data reprentation of a `pbxproj` file (#798) Resolves https://github.com/tuist/XcodeProj/issues/793 - Add the ability to instantiate `PBXProj` from `Data` representation - This complements https://github.com/tuist/XcodeProj/pull/787 where projects can now be serialised to `Data` representation rather than to disk Notes: - `PBXProj.name` isn't serialised as part of the serialised data representation and will need to be manually updated --- Sources/XcodeProj/Errors/Errors.swift | 3 ++ .../XcodeProj/Objects/Project/PBXProj.swift | 42 ++++++++++++++++++- .../Project/XcodeProjTests.swift | 14 +++++++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/Sources/XcodeProj/Errors/Errors.swift b/Sources/XcodeProj/Errors/Errors.swift index 6554f83fa..9dffd2468 100644 --- a/Sources/XcodeProj/Errors/Errors.swift +++ b/Sources/XcodeProj/Errors/Errors.swift @@ -183,6 +183,7 @@ enum PBXProjError: Error, CustomStringConvertible, Equatable { case pathIsAbsolute(Path) case multipleLocalPackages(productName: String) case multipleRemotePackages(productName: String) + case malformed var description: String { switch self { case let .notFound(path): @@ -201,6 +202,8 @@ enum PBXProjError: Error, CustomStringConvertible, Equatable { return "Found multiple top-level packages named \(productName)" case let .multipleRemotePackages(productName: productName): return "Can not resolve dependency \(productName) - conflicting version requirements" + case .malformed: + return "The .pbxproj is malformed." } } } diff --git a/Sources/XcodeProj/Objects/Project/PBXProj.swift b/Sources/XcodeProj/Objects/Project/PBXProj.swift index 71e241bd2..6248aed4e 100644 --- a/Sources/XcodeProj/Objects/Project/PBXProj.swift +++ b/Sources/XcodeProj/Objects/Project/PBXProj.swift @@ -66,6 +66,41 @@ public final class PBXProj: Decodable { objects: pbxproj.objects ) } + + /// Initializes the project with the data representation of pbxproj file. + /// + /// - Parameters: + /// - data: data representation of pbxproj file. + public convenience init(data: Data) throws { + var propertyListFormat = PropertyListSerialization.PropertyListFormat.xml + + let serialized = try PropertyListSerialization.propertyList( + from: data, + options: .mutableContainersAndLeaves, + format: &propertyListFormat + ) + + guard let pbxProjDictionary = serialized as? [String: Any] else { + throw PBXProjError.malformed + } + + let context = ProjectDecodingContext( + pbxProjValueReader: { key in + pbxProjDictionary[key] + } + ) + + let plistDecoder = XcodeprojPropertyListDecoder(context: context) + let pbxproj: PBXProj = try plistDecoder.decode(PBXProj.self, from: data) + + self.init( + rootObject: pbxproj.rootObject, + objectVersion: pbxproj.objectVersion, + archiveVersion: pbxproj.archiveVersion, + classes: pbxproj.classes, + objects: pbxproj.objects + ) + } private init( rootObject: PBXProject? = nil, @@ -146,8 +181,11 @@ public final class PBXProj: Decodable { options: .mutableContainersAndLeaves, format: &propertyListFormat ) - // swiftlint:disable:next force_cast - let pbxProjDictionary = serialized as! [String: Any] + + guard let pbxProjDictionary = serialized as? [String: Any] else { + throw PBXProjError.malformed + } + return (plistXML, pbxProjDictionary) } } diff --git a/Tests/XcodeProjTests/Project/XcodeProjTests.swift b/Tests/XcodeProjTests/Project/XcodeProjTests.swift index dabb8348d..3f4a81948 100644 --- a/Tests/XcodeProjTests/Project/XcodeProjTests.swift +++ b/Tests/XcodeProjTests/Project/XcodeProjTests.swift @@ -25,6 +25,20 @@ final class XcodeProjIntegrationTests: XCTestCase { initModel: XcodeProj.init(path:)) } + func test_initialize_PBXProj_with_data() throws { + // Given + let pbxprojPath = iosProjectPath + "project.pbxproj" + let pbxprojFromDisk = try PBXProj(path: pbxprojPath) + let pbxprojData = try Data(contentsOf: pbxprojPath.url) + + // When + let pbxprojFromData = try PBXProj(data: pbxprojData) + try pbxprojFromData.updateProjectName(path: pbxprojPath) + + // Then + XCTAssertEqual(pbxprojFromData, pbxprojFromDisk) + } + func test_write_includes_workspace_settings() throws { // Define workspace settings that should be written let workspaceSettings = WorkspaceSettings(buildSystem: .new, derivedDataLocationStyle: .default, autoCreateSchemes: false)