1+ import Foundation
2+
3+ // MARK: - FileItem
4+ class FileItem : ObservableObject {
5+ @Published var url : URL ?
6+ var name : String { url? . lastPathComponent ?? " " }
7+ }
8+
9+ // MARK: - CertificateFileManager
10+ class CertificateFileManager {
11+ static let shared = CertificateFileManager ( )
12+ let fileManager = FileManager . default
13+ let certificatesDirectory : URL
14+
15+ private init ( ) {
16+ let documentsDirectory = fileManager. urls ( for: . documentDirectory, in: . userDomainMask) . first!
17+ certificatesDirectory = documentsDirectory. appendingPathComponent ( " certificates " )
18+ createCertificatesDirectoryIfNeeded ( )
19+ }
20+
21+ private func createCertificatesDirectoryIfNeeded( ) {
22+ if !fileManager. fileExists ( atPath: certificatesDirectory. path) {
23+ try ? fileManager. createDirectory ( at: certificatesDirectory, withIntermediateDirectories: true )
24+ }
25+ }
26+
27+ func loadCertificates( ) -> [ CustomCertificate ] {
28+ var resultCerts : [ CustomCertificate ] = [ ]
29+ guard let folders = try ? fileManager. contentsOfDirectory ( at: certificatesDirectory, includingPropertiesForKeys: nil , options: [ . skipsHiddenFiles] ) else {
30+ return [ ]
31+ }
32+
33+ for folder in folders {
34+ let nameURL = folder. appendingPathComponent ( " name.txt " )
35+ if fileManager. fileExists ( atPath: nameURL. path) {
36+ if let nameData = try ? Data ( contentsOf: nameURL) ,
37+ let nameString = String ( data: nameData, encoding: . utf8) {
38+ resultCerts. append ( CustomCertificate ( displayName: nameString, folderName: folder. lastPathComponent) )
39+ }
40+ } else {
41+ // Fallback display name if missing
42+ resultCerts. append ( CustomCertificate ( displayName: folder. lastPathComponent, folderName: folder. lastPathComponent) )
43+ }
44+ }
45+
46+ return resultCerts
47+ }
48+
49+ func saveCertificate( p12Data: Data , provData: Data , password: String , displayName: String ) throws -> String {
50+ let baseName = sanitizeFileName ( displayName. isEmpty ? " Custom Certificate " : displayName)
51+ let p12HashNew = CertificatesManager . sha256Hex ( p12Data)
52+ let provHashNew = CertificatesManager . sha256Hex ( provData)
53+ let passwordHashNew = CertificatesManager . sha256Hex ( password. data ( using: . utf8) ?? Data ( ) )
54+
55+ // Check if identical cert already exists
56+ let existingFolders = try fileManager. contentsOfDirectory ( at: certificatesDirectory, includingPropertiesForKeys: nil , options: [ . skipsHiddenFiles] )
57+ for folder in existingFolders {
58+ let p12URL = folder. appendingPathComponent ( " certificate.p12 " )
59+ let provURL = folder. appendingPathComponent ( " profile.mobileprovision " )
60+ let passwordURL = folder. appendingPathComponent ( " password.txt " )
61+ if fileManager. fileExists ( atPath: p12URL. path) && fileManager. fileExists ( atPath: provURL. path) && fileManager. fileExists ( atPath: passwordURL. path) {
62+ do {
63+ let existingP12Data = try Data ( contentsOf: p12URL)
64+ let existingProvData = try Data ( contentsOf: provURL)
65+ let existingPasswordData = try Data ( contentsOf: passwordURL)
66+ let existingPassword = String ( data: existingPasswordData, encoding: . utf8) ?? " "
67+
68+ let p12HashExisting = CertificatesManager . sha256Hex ( existingP12Data)
69+ let provHashExisting = CertificatesManager . sha256Hex ( existingProvData)
70+ let passwordHashExisting = CertificatesManager . sha256Hex ( existingPassword. data ( using: . utf8) ?? Data ( ) )
71+
72+ if p12HashNew == p12HashExisting && provHashNew == provHashExisting && passwordHashNew == passwordHashExisting {
73+ throw NSError ( domain: " CertificateFileManager " , code: 2 , userInfo: [ NSLocalizedDescriptionKey: " This certificate already exists " ] )
74+ }
75+ } catch {
76+ // Skip if can't read existing
77+ continue
78+ }
79+ }
80+ }
81+
82+ // Create folder
83+ var finalName = baseName
84+ var counter = 1
85+ var folderURL = certificatesDirectory. appendingPathComponent ( finalName)
86+ while fileManager. fileExists ( atPath: folderURL. path) {
87+ counter += 1
88+ finalName = " \( baseName) - \( counter) "
89+ folderURL = certificatesDirectory. appendingPathComponent ( finalName)
90+ }
91+ try fileManager. createDirectory ( at: folderURL, withIntermediateDirectories: true )
92+
93+ try p12Data. write ( to: folderURL. appendingPathComponent ( " certificate.p12 " ) )
94+ try provData. write ( to: folderURL. appendingPathComponent ( " profile.mobileprovision " ) )
95+ try password. data ( using: . utf8) ? . write ( to: folderURL. appendingPathComponent ( " password.txt " ) )
96+ let displayToWrite = uniqueDisplayName ( displayName, excludingFolder: nil )
97+ try displayToWrite. data ( using: . utf8) ? . write ( to: folderURL. appendingPathComponent ( " name.txt " ) )
98+
99+ return finalName
100+ }
101+
102+ func updateCertificate( folderName: String , p12Data: Data , provData: Data , password: String , displayName: String ) throws {
103+ let certificateFolder = certificatesDirectory. appendingPathComponent ( folderName)
104+ let p12HashNew = CertificatesManager . sha256Hex ( p12Data)
105+ let provHashNew = CertificatesManager . sha256Hex ( provData)
106+ let passwordHashNew = CertificatesManager . sha256Hex ( password. data ( using: . utf8) ?? Data ( ) )
107+
108+ // Prevent accidental duplicate update matching another cert
109+ let existingFolders = try fileManager. contentsOfDirectory ( at: certificatesDirectory, includingPropertiesForKeys: nil , options: [ . skipsHiddenFiles] )
110+ for folder in existingFolders where folder. lastPathComponent != folderName {
111+ let p12URL = folder. appendingPathComponent ( " certificate.p12 " )
112+ let provURL = folder. appendingPathComponent ( " profile.mobileprovision " )
113+ let passwordURL = folder. appendingPathComponent ( " password.txt " )
114+ if fileManager. fileExists ( atPath: p12URL. path) && fileManager. fileExists ( atPath: provURL. path) && fileManager. fileExists ( atPath: passwordURL. path) {
115+ do {
116+ let existingP12Data = try Data ( contentsOf: p12URL)
117+ let existingProvData = try Data ( contentsOf: provURL)
118+ let existingPasswordData = try Data ( contentsOf: passwordURL)
119+ let existingPassword = String ( data: existingPasswordData, encoding: . utf8) ?? " "
120+
121+ let p12HashExisting = CertificatesManager . sha256Hex ( existingP12Data)
122+ let provHashExisting = CertificatesManager . sha256Hex ( existingProvData)
123+ let passwordHashExisting = CertificatesManager . sha256Hex ( existingPassword. data ( using: . utf8) ?? Data ( ) )
124+
125+ if p12HashNew == p12HashExisting && provHashNew == provHashExisting && passwordHashNew == passwordHashExisting {
126+ throw NSError ( domain: " CertificateFileManager " , code: 2 , userInfo: [ NSLocalizedDescriptionKey: " This updated certificate matches another existing one " ] )
127+ }
128+ } catch {
129+ // Skip if can't read existing
130+ continue
131+ }
132+ }
133+ }
134+
135+ // Overwrite files
136+ try p12Data. write ( to: certificateFolder. appendingPathComponent ( " certificate.p12 " ) )
137+ try provData. write ( to: certificateFolder. appendingPathComponent ( " profile.mobileprovision " ) )
138+ try password. data ( using: . utf8) ? . write ( to: certificateFolder. appendingPathComponent ( " password.txt " ) )
139+ let displayToWrite = uniqueDisplayName ( displayName, excludingFolder: folderName)
140+ try displayToWrite. data ( using: . utf8) ? . write ( to: certificateFolder. appendingPathComponent ( " name.txt " ) )
141+ }
142+
143+ func deleteCertificate( folderName: String ) throws {
144+ let certificateFolder = certificatesDirectory. appendingPathComponent ( folderName)
145+ try fileManager. removeItem ( at: certificateFolder)
146+ }
147+
148+ private func sanitizeFileName( _ name: String ) -> String {
149+ let invalidChars = CharacterSet ( charactersIn: " :/ \\ ?%*| \" <> " )
150+ return name. components ( separatedBy: invalidChars) . joined ( separator: " _ " )
151+ }
152+
153+ // Return a unique display name by appending " 2", " 3", ... if needed.
154+ // `excludingFolder` lets updateCertificate keep the current folder's name out of the conflict check.
155+ private func uniqueDisplayName( _ desired: String , excludingFolder: String ? = nil ) -> String {
156+ let base = desired. isEmpty ? " Custom Certificate " : desired
157+ var existingNames = Set < String > ( )
158+ if let folders = try ? fileManager. contentsOfDirectory ( at: certificatesDirectory, includingPropertiesForKeys: nil , options: [ . skipsHiddenFiles] ) {
159+ for folder in folders {
160+ if folder. lastPathComponent == excludingFolder { continue }
161+ let nameURL = folder. appendingPathComponent ( " name.txt " )
162+ if let data = try ? Data ( contentsOf: nameURL) , let s = String ( data: data, encoding: . utf8) {
163+ existingNames. insert ( s)
164+ } else {
165+ // fallback to folder name if name.txt missing
166+ existingNames. insert ( folder. lastPathComponent)
167+ }
168+ }
169+ }
170+
171+ if !existingNames. contains ( base) {
172+ return base
173+ }
174+
175+ var counter = 2
176+ while existingNames. contains ( " \( base) \( counter) " ) {
177+ counter += 1
178+ }
179+ return " \( base) \( counter) "
180+ }
181+ }
182+
183+ // MARK: - PickerKind Enum
184+ enum PickerKind : Identifiable {
185+ case ipa, p12, prov
186+ var id : Int {
187+ switch self {
188+ case . ipa: return 0
189+ case . p12: return 1
190+ case . prov: return 2
191+ }
192+ }
193+ }
194+ enum CertificatePickerKind : Identifiable {
195+ case p12, prov
196+ var id : Self { self }
197+ }
0 commit comments