Skip to content
This repository was archived by the owner on Mar 7, 2026. It is now read-only.

Commit 837168c

Browse files
authored
Move all non-UI logic into ModelsAndManagers.swift and fix FileItem binding issues in SignerView
1 parent db788d5 commit 837168c

4 files changed

Lines changed: 200 additions & 200 deletions

File tree

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
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

Comments
 (0)