StikDebug/StikDebug/Services/DeveloperDiskImageService.swift
2026-09-23 17:09:26 -06:00

152 lines
5.8 KiB
Swift

//
// DeveloperDiskImageService.swift
// StikDebug
//
import Foundation
final class DeveloperDiskImageService {
static let shared = DeveloperDiskImageService()
static let directoryURL = URL.documentsDirectory.appendingPathComponent("DDI", isDirectory: true)
static var filesAreReady: Bool {
downloadItems.allSatisfy {
FileManager.default.fileExists(atPath: directoryURL.appendingPathComponent($0.fileName).path)
}
}
private let fileManager: FileManager
private let session: URLSession
init(fileManager: FileManager = .default, session: URLSession = .shared) {
self.fileManager = fileManager
self.session = session
}
func downloadMissingFiles() async throws {
let hasCryptexMarker = ["Image.dmg.cryptex_info", "Image.dmg.root_hash"].contains {
fileManager.fileExists(atPath: Self.directoryURL.appendingPathComponent($0).path)
}
let replaceLegacyFiles = !hasCryptexMarker && Self.downloadItems.contains {
fileManager.fileExists(atPath: Self.directoryURL.appendingPathComponent($0.fileName).path)
}
for item in Self.downloadItems {
let destinationURL = Self.directoryURL.appendingPathComponent(item.fileName)
guard replaceLegacyFiles || !fileManager.fileExists(atPath: destinationURL.path) else {
continue
}
try await downloadFile(from: item.urlString, to: destinationURL)
}
}
func downloadFile(from urlString: String, to destinationURL: URL) async throws {
guard let url = URL(string: urlString),
url.scheme?.lowercased() == "https" else {
throw DDIDownloadError.invalidURL(urlString)
}
let (temporaryURL, response) = try await session.download(from: url)
guard let httpResponse = response as? HTTPURLResponse else {
throw DDIDownloadError.invalidResponse
}
guard (200..<300).contains(httpResponse.statusCode) else {
throw DDIDownloadError.badStatus(httpResponse.statusCode)
}
try fileManager.createDirectory(
at: destinationURL.deletingLastPathComponent(),
withIntermediateDirectories: true
)
if fileManager.fileExists(atPath: destinationURL.path) {
try fileManager.removeItem(at: destinationURL)
}
try fileManager.moveItem(at: temporaryURL, to: destinationURL)
}
func redownload(progressHandler: ((Double, String) -> Void)? = nil) async throws {
let totalStages = Double(Self.downloadItems.count + 1)
var completedStages = 0.0
progressHandler?(0.0, "Removing existing DDI files...")
let completionMarker = Self.directoryURL.appendingPathComponent("Image.dmg.root_hash")
if fileManager.fileExists(atPath: completionMarker.path) {
try fileManager.removeItem(at: completionMarker)
}
for item in Self.downloadItems {
let fileURL = Self.directoryURL.appendingPathComponent(item.fileName)
if fileManager.fileExists(atPath: fileURL.path) {
try fileManager.removeItem(at: fileURL)
}
}
completedStages += 1.0
progressHandler?(completedStages / totalStages, "Starting downloads...")
for item in Self.downloadItems {
progressHandler?(completedStages / totalStages, "Downloading \(item.name)...")
let destinationURL = Self.directoryURL.appendingPathComponent(item.fileName)
try await downloadFile(from: item.urlString, to: destinationURL)
completedStages += 1.0
progressHandler?(completedStages / totalStages, "\(item.name) ready")
}
progressHandler?(1.0, "DDI download complete.")
}
private static let downloadItems: [DDIDownloadItem] = [
.init(
name: "Build Manifest",
fileName: "BuildManifest.plist",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/BuildManifest.plist"
),
.init(
name: "Image",
fileName: "Image.dmg",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/Image.dmg"
),
.init(
name: "TrustCache",
fileName: "Image.dmg.trustcache",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/Image.dmg.trustcache"
),
.init(
name: "Cryptex Info",
fileName: "Image.dmg.cryptex_info",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/Image.dmg.cryptex_info"
),
.init(
name: "Root Hash",
fileName: "Image.dmg.root_hash",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/Image.dmg.root_hash"
)
]
}
private struct DDIDownloadItem {
let name: String
let fileName: String
let urlString: String
}
enum DDIDownloadError: LocalizedError {
case invalidURL(String)
case invalidResponse
case badStatus(Int)
var errorDescription: String? {
switch self {
case .invalidURL(let string):
return "Invalid download URL: \(string)"
case .invalidResponse:
return "The DDI server returned an invalid response."
case .badStatus(let statusCode):
return "The DDI server returned HTTP \(statusCode)."
}
}
}
func redownloadDDI(progressHandler: ((Double, String) -> Void)? = nil) async throws {
try await DeveloperDiskImageService.shared.redownload(progressHandler: progressHandler)
}