mirror of
https://github.com/ichmagmaus111/ghostgram.git
synced 2026-11-03 14:17:19 +01:00
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
92 lines
4.5 KiB
Swift
92 lines
4.5 KiB
Swift
import Foundation
|
|
import Metal
|
|
import ObjectiveC
|
|
|
|
/// Serializes binary archives so that a failure is thrown rather than crashing the process.
|
|
///
|
|
/// `serialize(to:)` ends by moving the file Metal compiled (`<target directory>/<UUID>.metallib`) over the target with
|
|
/// `-[NSFileManager replaceItemAtURL:withItemAtURL:backupItemName:options:resultingItemURL:error:]`. Metal makes that
|
|
/// call in the compiler's completion handler, on another thread, and keeps the error it reports without a reference of
|
|
/// its own: the error is freed when that thread's autorelease pool drains, and the thread that called `serialize(to:)`
|
|
/// then retains it and crashes (`objc_retain` in `-[_MTLBinaryArchive airntSerializeToURL:options:error:]`; seen on
|
|
/// iOS 26.3, reproduced on macOS 27). So whatever makes that move fail, such as the directory or Metal's file
|
|
/// disappearing or a target that cannot be replaced, is a crash instead of an error.
|
|
///
|
|
/// While a target is being serialized, the move into it is watched, and the error it fails with is kept alive until
|
|
/// `serialize(to:)` has returned. Every other use of that method passes straight through.
|
|
@available(macOS 11.0, iOS 14.0, *)
|
|
enum MetalBinaryArchiveSerialization {
|
|
struct UnprotectedError: Error {
|
|
}
|
|
|
|
static func serialize(_ archive: MTLBinaryArchive, to url: URL) throws {
|
|
// Without the hook, a failed move is a crash: rather not save at all.
|
|
guard let failedMoveErrors = FailedMoveErrors.shared else {
|
|
throw UnprotectedError()
|
|
}
|
|
failedMoveErrors.beginKeeping(forTarget: url)
|
|
defer {
|
|
failedMoveErrors.endKeeping(forTarget: url)
|
|
}
|
|
try archive.serialize(to: url)
|
|
}
|
|
}
|
|
|
|
/// The errors of failed moves into the targets of the serializations in progress.
|
|
final class FailedMoveErrors {
|
|
private typealias ReplaceItemImplementation = @convention(c) (AnyObject, Selector, AnyObject?, AnyObject?, AnyObject?, UInt, UnsafeMutableRawPointer?, UnsafeMutableRawPointer?) -> ObjCBool
|
|
private typealias ReplaceItemBlock = @convention(block) (AnyObject, AnyObject?, AnyObject?, AnyObject?, UInt, UnsafeMutableRawPointer?, UnsafeMutableRawPointer?) -> ObjCBool
|
|
|
|
/// Nil when the move cannot be watched.
|
|
static let shared: FailedMoveErrors? = FailedMoveErrors()
|
|
|
|
private let lock = NSLock()
|
|
/// By target path. A target is only serialized once at a time: the cache names each one with a new UUID.
|
|
private var keptErrors: [String: [AnyObject]] = [:]
|
|
|
|
private init?() {
|
|
let selector = NSSelectorFromString("replaceItemAtURL:withItemAtURL:backupItemName:options:resultingItemURL:error:")
|
|
guard let method = class_getInstanceMethod(FileManager.self, selector) else {
|
|
return nil
|
|
}
|
|
let original = unsafeBitCast(method_getImplementation(method), to: ReplaceItemImplementation.self)
|
|
let replacement: ReplaceItemBlock = { [unowned self] fileManager, originalItemUrl, newItemUrl, backupItemName, options, resultingItemUrl, error in
|
|
let result = original(fileManager, selector, originalItemUrl, newItemUrl, backupItemName, options, resultingItemUrl, error)
|
|
if !result.boolValue, let error, let path = (originalItemUrl as? NSURL)?.path {
|
|
self.keepError(at: error, forTargetPath: path)
|
|
}
|
|
return result
|
|
}
|
|
method_setImplementation(method, imp_implementationWithBlock(unsafeBitCast(replacement, to: AnyObject.self)))
|
|
}
|
|
|
|
func beginKeeping(forTarget url: URL) {
|
|
self.lock.lock()
|
|
self.keptErrors[url.path] = []
|
|
self.lock.unlock()
|
|
}
|
|
|
|
/// Releases what was kept for the target; whoever received the error holds its own reference by now.
|
|
func endKeeping(forTarget url: URL) {
|
|
self.lock.lock()
|
|
let errors = self.keptErrors.removeValue(forKey: url.path)
|
|
self.lock.unlock()
|
|
// Released outside the lock.
|
|
withExtendedLifetime(errors) {
|
|
}
|
|
}
|
|
|
|
/// Called after a failed move, whose error slot is then written. Other callers' slots are never read.
|
|
private func keepError(at slot: UnsafeMutableRawPointer, forTargetPath path: String) {
|
|
self.lock.lock()
|
|
defer {
|
|
self.lock.unlock()
|
|
}
|
|
guard self.keptErrors[path] != nil, let error = slot.load(as: Unmanaged<AnyObject>?.self) else {
|
|
return
|
|
}
|
|
// Still alive here: it is autoreleased into this thread's pool, which has not drained yet.
|
|
self.keptErrors[path]?.append(error.takeUnretainedValue())
|
|
}
|
|
}
|