@@ -78,6 +78,181 @@ struct ActivityView: UIViewControllerRepresentable {
7878 func updateUIViewController( _ uiViewController: UIActivityViewController , context: Context ) { }
7979}
8080
81+ // MARK: - CertificateFileManager
82+
83+ class CertificateFileManager {
84+ static let shared = CertificateFileManager ( )
85+ let fileManager = FileManager . default
86+ let certificatesDirectory : URL
87+
88+ private init ( ) {
89+ let documentsDirectory = fileManager. urls ( for: . documentDirectory, in: . userDomainMask) . first!
90+ certificatesDirectory = documentsDirectory. appendingPathComponent ( " certificates " )
91+ createCertificatesDirectoryIfNeeded ( )
92+ }
93+
94+ private func createCertificatesDirectoryIfNeeded( ) {
95+ if !fileManager. fileExists ( atPath: certificatesDirectory. path) {
96+ try ? fileManager. createDirectory ( at: certificatesDirectory, withIntermediateDirectories: true )
97+ }
98+ }
99+
100+ func loadCertificates( ) -> [ CustomCertificate ] {
101+ var resultCerts : [ CustomCertificate ] = [ ]
102+ guard let folders = try ? fileManager. contentsOfDirectory ( at: certificatesDirectory, includingPropertiesForKeys: nil , options: [ . skipsHiddenFiles] ) else {
103+ return [ ]
104+ }
105+
106+ for folder in folders {
107+ let nameURL = folder. appendingPathComponent ( " name.txt " )
108+ if fileManager. fileExists ( atPath: nameURL. path) {
109+ if let nameData = try ? Data ( contentsOf: nameURL) ,
110+ let nameString = String ( data: nameData, encoding: . utf8) {
111+ resultCerts. append ( CustomCertificate ( displayName: nameString, folderName: folder. lastPathComponent) )
112+ }
113+ } else {
114+ // Fallback display name if missing
115+ resultCerts. append ( CustomCertificate ( displayName: folder. lastPathComponent, folderName: folder. lastPathComponent) )
116+ }
117+ }
118+
119+ return resultCerts
120+ }
121+
122+ func saveCertificate( p12Data: Data , provData: Data , password: String , displayName: String ) throws -> String {
123+ let baseName = sanitizeFileName ( displayName. isEmpty ? " Custom Certificate " : displayName)
124+ let p12HashNew = CertificatesManager . sha256Hex ( p12Data)
125+ let provHashNew = CertificatesManager . sha256Hex ( provData)
126+ let passwordHashNew = CertificatesManager . sha256Hex ( password. data ( using: . utf8) ?? Data ( ) )
127+
128+ // Check if identical cert already exists
129+ let existingFolders = try fileManager. contentsOfDirectory ( at: certificatesDirectory, includingPropertiesForKeys: nil , options: [ . skipsHiddenFiles] )
130+ for folder in existingFolders {
131+ let p12URL = folder. appendingPathComponent ( " certificate.p12 " )
132+ let provURL = folder. appendingPathComponent ( " profile.mobileprovision " )
133+ let passwordURL = folder. appendingPathComponent ( " password.txt " )
134+ if fileManager. fileExists ( atPath: p12URL. path) && fileManager. fileExists ( atPath: provURL. path) && fileManager. fileExists ( atPath: passwordURL. path) {
135+ do {
136+ let existingP12Data = try Data ( contentsOf: p12URL)
137+ let existingProvData = try Data ( contentsOf: provURL)
138+ let existingPasswordData = try Data ( contentsOf: passwordURL)
139+ let existingPassword = String ( data: existingPasswordData, encoding: . utf8) ?? " "
140+
141+ let p12HashExisting = CertificatesManager . sha256Hex ( existingP12Data)
142+ let provHashExisting = CertificatesManager . sha256Hex ( existingProvData)
143+ let passwordHashExisting = CertificatesManager . sha256Hex ( existingPassword. data ( using: . utf8) ?? Data ( ) )
144+
145+ if p12HashNew == p12HashExisting && provHashNew == provHashExisting && passwordHashNew == passwordHashExisting {
146+ throw NSError ( domain: " CertificateFileManager " , code: 2 , userInfo: [ NSLocalizedDescriptionKey: " This certificate already exists " ] )
147+ }
148+ } catch {
149+ // Skip if can't read existing
150+ continue
151+ }
152+ }
153+ }
154+
155+ // Create folder
156+ var finalName = baseName
157+ var counter = 1
158+ var folderURL = certificatesDirectory. appendingPathComponent ( finalName)
159+ while fileManager. fileExists ( atPath: folderURL. path) {
160+ counter += 1
161+ finalName = " \( baseName) - \( counter) "
162+ folderURL = certificatesDirectory. appendingPathComponent ( finalName)
163+ }
164+ try fileManager. createDirectory ( at: folderURL, withIntermediateDirectories: true )
165+
166+ try p12Data. write ( to: folderURL. appendingPathComponent ( " certificate.p12 " ) )
167+ try provData. write ( to: folderURL. appendingPathComponent ( " profile.mobileprovision " ) )
168+ try password. data ( using: . utf8) ? . write ( to: folderURL. appendingPathComponent ( " password.txt " ) )
169+ let displayToWrite = uniqueDisplayName ( displayName, excludingFolder: nil )
170+ try displayToWrite. data ( using: . utf8) ? . write ( to: folderURL. appendingPathComponent ( " name.txt " ) )
171+
172+ return finalName
173+ }
174+
175+ func updateCertificate( folderName: String , p12Data: Data , provData: Data , password: String , displayName: String ) throws {
176+ let certificateFolder = certificatesDirectory. appendingPathComponent ( folderName)
177+ let p12HashNew = CertificatesManager . sha256Hex ( p12Data)
178+ let provHashNew = CertificatesManager . sha256Hex ( provData)
179+ let passwordHashNew = CertificatesManager . sha256Hex ( password. data ( using: . utf8) ?? Data ( ) )
180+
181+ // Prevent accidental duplicate update matching another cert
182+ let existingFolders = try fileManager. contentsOfDirectory ( at: certificatesDirectory, includingPropertiesForKeys: nil , options: [ . skipsHiddenFiles] )
183+ for folder in existingFolders where folder. lastPathComponent != folderName {
184+ let p12URL = folder. appendingPathComponent ( " certificate.p12 " )
185+ let provURL = folder. appendingPathComponent ( " profile.mobileprovision " )
186+ let passwordURL = folder. appendingPathComponent ( " password.txt " )
187+ if fileManager. fileExists ( atPath: p12URL. path) && fileManager. fileExists ( atPath: provURL. path) && fileManager. fileExists ( atPath: passwordURL. path) {
188+ do {
189+ let existingP12Data = try Data ( contentsOf: p12URL)
190+ let existingProvData = try Data ( contentsOf: provURL)
191+ let existingPasswordData = try Data ( contentsOf: passwordURL)
192+ let existingPassword = String ( data: existingPasswordData, encoding: . utf8) ?? " "
193+
194+ let p12HashExisting = CertificatesManager . sha256Hex ( existingP12Data)
195+ let provHashExisting = CertificatesManager . sha256Hex ( existingProvData)
196+ let passwordHashExisting = CertificatesManager . sha256Hex ( existingPassword. data ( using: . utf8) ?? Data ( ) )
197+
198+ if p12HashNew == p12HashExisting && provHashNew == provHashExisting && passwordHashNew == passwordHashExisting {
199+ throw NSError ( domain: " CertificateFileManager " , code: 2 , userInfo: [ NSLocalizedDescriptionKey: " This updated certificate matches another existing one " ] )
200+ }
201+ } catch {
202+ // Skip if can't read existing
203+ continue
204+ }
205+ }
206+ }
207+
208+ // Overwrite files
209+ try p12Data. write ( to: certificateFolder. appendingPathComponent ( " certificate.p12 " ) )
210+ try provData. write ( to: certificateFolder. appendingPathComponent ( " profile.mobileprovision " ) )
211+ try password. data ( using: . utf8) ? . write ( to: certificateFolder. appendingPathComponent ( " password.txt " ) )
212+ let displayToWrite = uniqueDisplayName ( displayName, excludingFolder: folderName)
213+ try displayToWrite. data ( using: . utf8) ? . write ( to: certificateFolder. appendingPathComponent ( " name.txt " ) )
214+ }
215+
216+ func deleteCertificate( folderName: String ) throws {
217+ let certificateFolder = certificatesDirectory. appendingPathComponent ( folderName)
218+ try fileManager. removeItem ( at: certificateFolder)
219+ }
220+
221+ private func sanitizeFileName( _ name: String ) -> String {
222+ let invalidChars = CharacterSet ( charactersIn: " :/ \\ ?%*| \" <> " )
223+ return name. components ( separatedBy: invalidChars) . joined ( separator: " _ " )
224+ }
225+
226+ // Return a unique display name by appending " 2", " 3", ... if needed.
227+ // `excludingFolder` lets updateCertificate keep the current folder's name out of the conflict check.
228+ private func uniqueDisplayName( _ desired: String , excludingFolder: String ? = nil ) -> String {
229+ let base = desired. isEmpty ? " Custom Certificate " : desired
230+ var existingNames = Set < String > ( )
231+ if let folders = try ? fileManager. contentsOfDirectory ( at: certificatesDirectory, includingPropertiesForKeys: nil , options: [ . skipsHiddenFiles] ) {
232+ for folder in folders {
233+ if folder. lastPathComponent == excludingFolder { continue }
234+ let nameURL = folder. appendingPathComponent ( " name.txt " )
235+ if let data = try ? Data ( contentsOf: nameURL) , let s = String ( data: data, encoding: . utf8) {
236+ existingNames. insert ( s)
237+ } else {
238+ // fallback to folder name if name.txt missing
239+ existingNames. insert ( folder. lastPathComponent)
240+ }
241+ }
242+ }
243+
244+ if !existingNames. contains ( base) {
245+ return base
246+ }
247+
248+ var counter = 2
249+ while existingNames. contains ( " \( base) \( counter) " ) {
250+ counter += 1
251+ }
252+ return " \( base) \( counter) "
253+ }
254+ }
255+
81256// MARK: - PickerKind Enum
82257
83258enum PickerKind : Identifiable {
0 commit comments