1+ // signer.swift
12import Foundation
23import ZIPFoundation
34import ZsignSwift
4-
5+
56public enum signer {
67 public static func sign(
78 ipaURL: URL ,
89 p12URL: URL ,
910 provURL: URL ,
1011 p12Password: String ,
11- progressUpdate: @escaping ( String ) -> Void = { _ in } ,
12+ progressUpdate: @escaping ( String , Double ) -> Void = { _ , _ in } ,
1213 completion: @escaping ( Result < URL , Error > ) -> Void
1314 ) {
1415 SigningManager . sign (
@@ -47,19 +48,19 @@ public enum signer {
4748 return expDate
4849 }
4950}
50-
51+
5152fileprivate class SigningManager {
5253 static func sign(
5354 ipaURL: URL ,
5455 p12URL: URL ,
5556 provURL: URL ,
5657 p12Password: String ,
57- progressUpdate: @escaping ( String ) -> Void ,
58+ progressUpdate: @escaping ( String , Double ) -> Void ,
5859 completion: @escaping ( Result < URL , Error > ) -> Void
5960 ) {
6061 DispatchQueue . global ( qos: . userInitiated) . async {
6162 do {
62- progressUpdate ( " Preparing files 📂 " )
63+ progressUpdate ( " Preparing files 📂 " , 0.0 )
6364 let ( tmpRoot, inputsDir, workDir) = try prepareTemporaryWorkspace ( )
6465 defer {
6566 cleanupTemporaryFiles ( at: tmpRoot)
@@ -70,13 +71,16 @@ fileprivate class SigningManager {
7071 provURL: provURL,
7172 to: inputsDir
7273 )
73- progressUpdate ( " Unzipping IPA 🔓 " )
74- try extractIPA ( ipaURL: localIPA, to: workDir, progressUpdate: progressUpdate)
74+ progressUpdate ( " Unzipping IPA 🔓 " , 0.25 )
75+ try extractIPA ( ipaURL: localIPA, to: workDir, progressUpdate: { status, progress in
76+ progressUpdate ( status, 0.25 + ( progress * 0.25 ) )
77+ } )
7578 let payloadDir = workDir. appendingPathComponent ( " Payload " )
7679 let appDir = try findAppBundle ( in: payloadDir)
77- progressUpdate ( " Signing \( appDir. lastPathComponent) ✍️ " )
80+ progressUpdate ( " Signing \( appDir. lastPathComponent) ✍️ " , 0.5 )
7881 let sema = DispatchSemaphore ( value: 0 )
7982 var signingError : Error ?
83+
8084 // Use the ZsignSwift API
8185 _ = Zsign . sign (
8286 appPath: appDir. path,
@@ -90,23 +94,27 @@ fileprivate class SigningManager {
9094 sema. signal ( )
9195 }
9296 sema. wait ( )
97+
9398 if let error = signingError {
9499 throw error
95100 }
96- progressUpdate ( " Zipping signed IPA 📦 " )
101+
102+ progressUpdate ( " Zipping signed IPA 📦 " , 0.75 )
97103 let signedIPAURL = try createSignedIPA (
98104 from: workDir,
99105 originalIPAURL: ipaURL,
100106 outputDir: tmpRoot,
101- progressUpdate: progressUpdate
107+ progressUpdate: { status, progress in
108+ progressUpdate ( status, 0.75 + ( progress * 0.25 ) )
109+ }
102110 )
103111 completion ( . success( signedIPAURL) )
104112 } catch {
105113 completion ( . failure( error) )
106114 }
107115 }
108116 }
109-
117+
110118 // MARK: - workspace helpers
111119 static func prepareTemporaryWorkspace( ) throws -> ( URL , URL , URL ) {
112120 let fm = FileManager . default
@@ -117,7 +125,7 @@ fileprivate class SigningManager {
117125 try fm. createDirectory ( at: work, withIntermediateDirectories: true )
118126 return ( tmpRoot, inputs, work)
119127 }
120-
128+
121129 static func copyInputFiles(
122130 ipaURL: URL ,
123131 p12URL: URL ,
@@ -138,24 +146,24 @@ fileprivate class SigningManager {
138146 try fm. copyItem ( at: provURL, to: localProv)
139147 return ( localIPA, localP12, localProv)
140148 }
141-
149+
142150 static func extractIPA(
143151 ipaURL: URL ,
144152 to workDir: URL ,
145- progressUpdate: @escaping ( String ) -> Void
153+ progressUpdate: @escaping ( String , Double ) -> Void
146154 ) throws {
147155 let fm = FileManager . default
148156 let progress = Progress ( )
149157 let observation = progress. observe ( \Progress . fractionCompleted) { prog, _ in
150158 let pct = Int ( prog. fractionCompleted * 100 )
151- progressUpdate ( " Unzipping IPA 🔓 ( \( pct) %) " )
159+ progressUpdate ( " Unzipping IPA 🔓 ( \( pct) %) " , prog . fractionCompleted )
152160 }
153161 defer {
154162 observation. invalidate ( )
155163 }
156164 try fm. unzipItem ( at: ipaURL, to: workDir, progress: progress)
157165 }
158-
166+
159167 static func findAppBundle( in payloadDir: URL ) throws -> URL {
160168 let fm = FileManager . default
161169 guard fm. fileExists ( atPath: payloadDir. path) else {
@@ -167,37 +175,51 @@ fileprivate class SigningManager {
167175 }
168176 return payloadDir. appendingPathComponent ( appName)
169177 }
170-
178+
171179 static func createSignedIPA(
172180 from workDir: URL ,
173181 originalIPAURL: URL ,
174182 outputDir: URL ,
175- progressUpdate: @escaping ( String ) -> Void
183+ progressUpdate: @escaping ( String , Double ) -> Void
176184 ) throws -> URL {
177185 let fm = FileManager . default
178186 let originalBase = originalIPAURL. deletingPathExtension ( ) . lastPathComponent
179- let finalFileName = " \( originalBase ) _signed_ \( UUID ( ) . uuidString) .ipa "
187+ let finalFileName = " signed_ \( UUID ( ) . uuidString) .ipa "
180188 let signedIpa = outputDir. appendingPathComponent ( finalFileName)
181189 let progress = Progress ( )
182190 let observation = progress. observe ( \Progress . fractionCompleted) { prog, _ in
183191 let pct = Int ( prog. fractionCompleted * 100 )
184- progressUpdate ( " Zipping signed IPA 📦 ( \( pct) %) " )
192+ progressUpdate ( " Zipping signed IPA 📦 ( \( pct) %) " , prog . fractionCompleted )
185193 }
186194 defer {
187195 observation. invalidate ( )
188196 }
189197 try fm. zipItem ( at: workDir, to: signedIpa, shouldKeepParent: false , progress: progress)
190- // Copy to Documents for sharing
191- let docs = fm. urls ( for: . documentDirectory, in: . userDomainMask) . first!
192- let outURL = docs. appendingPathComponent ( finalFileName)
193- if fm. fileExists ( atPath: outURL. path) {
194- try fm. removeItem ( at: outURL)
198+
199+ // Copy to AppFolder/temp for permanent storage
200+ let appFolder = getAppFolder ( )
201+ let tempDir = appFolder. appendingPathComponent ( " temp " )
202+ try fm. createDirectory ( at: tempDir, withIntermediateDirectories: true )
203+ let finalURL = tempDir. appendingPathComponent ( finalFileName)
204+ if fm. fileExists ( atPath: finalURL. path) {
205+ try fm. removeItem ( at: finalURL)
195206 }
196- try fm. copyItem ( at: signedIpa, to: outURL)
197- return outURL
207+ try fm. copyItem ( at: signedIpa, to: finalURL)
208+
209+ return finalURL
198210 }
199-
211+
200212 static func cleanupTemporaryFiles( at url: URL ) {
201213 try ? FileManager . default. removeItem ( at: url)
202214 }
215+
216+ private static func getAppFolder( ) -> URL {
217+ let fm = FileManager . default
218+ let documents = fm. urls ( for: . documentDirectory, in: . userDomainMask) . first!
219+ let appFolder = documents. appendingPathComponent ( " AppFolder " )
220+ if !fm. fileExists ( atPath: appFolder. path) {
221+ try ? fm. createDirectory ( at: appFolder, withIntermediateDirectories: true )
222+ }
223+ return appFolder
224+ }
203225}
0 commit comments