import Foundation import UIKit import CoreGraphics import CoreText import ImageIO import ModelIO import AVFoundation import CoreHaptics import Darwin // MARK: - Shared runners enum Runners { // ---- CoreGraphics PDF render (the in-process trigger for the CG family) @discardableResult static func renderPDF(_ data: Data, pixels: Int = 256, page: Int = 1) throws -> String { guard let provider = CGDataProvider(data: data as CFData) else { throw LabError(message: tl("CGDataProvider 创建失败")) } guard let document = CGPDFDocument(provider) else { throw LabError(message: tl("CGPDFDocument 解析失败(文件被拒绝)")) } guard let pg = document.page(at: page) else { throw LabError(message: tl("PDF 无第 ") + "\(page)" + tl(" 页")) } let bytesPerRow = pixels * 4 let buffer = UnsafeMutableRawPointer.allocate(byteCount: bytesPerRow * pixels, alignment: 16) defer { buffer.deallocate() } guard let ctx = CGContext(data: buffer, width: pixels, height: pixels, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { throw LabError(message: tl("CGBitmapContext 创建失败")) } ctx.setFillColor(CGColor(red: 1, green: 1, blue: 1, alpha: 1)) ctx.fill(CGRect(x: 0, y: 0, width: pixels, height: pixels)) let start = CFAbsoluteTimeGetCurrent() // === trigger === ctx.drawPDFPage(pg) let ms = Int((CFAbsoluteTimeGetCurrent() - start) * 1000) // sample the middle of the bitmap to prove the page actually rasterised let mid = bytesPerRow * (pixels / 2) + (pixels / 2) * 4 let ptr = buffer.assumingMemoryBound(to: UInt8.self) let sample = "\(ptr[mid]),\(ptr[mid + 1]),\(ptr[mid + 2]),\(ptr[mid + 3])" return tl("页面渲染完成 ") + "\(ms)" + tl(" ms;中心像素 RGBA = ") + "\(sample)" } // ---- ImageIO decode struct ImageResult { var frameCount: Int var decoded: Bool var width: Int var height: Int var properties: String var thumbnail: Bool } static func decodeImage(_ data: Data, wantThumbnail: Bool = false) throws -> ImageResult { guard let source = CGImageSourceCreateWithData(data as CFData, nil) else { throw LabError(message: tl("CGImageSource 拒绝该数据")) } let count = CGImageSourceGetCount(source) var width = 0, height = 0, decoded = false, thumbOK = false var props = "" if let dict = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any] { width = dict[kCGImagePropertyPixelWidth as String] as? Int ?? 0 height = dict[kCGImagePropertyPixelHeight as String] as? Int ?? 0 props = dict.keys.sorted().prefix(8).joined(separator: ", ") if let exif = dict[kCGImagePropertyExifDictionary as String] as? [String: Any] { if let comment = exif[kCGImagePropertyExifUserComment as String] as? Data { props += " | UserComment(\(comment.count)B)=\(comment.prefix(24).map { String(format: "%02x", $0) }.joined())" } else if let comment = exif[kCGImagePropertyExifUserComment as String] as? String { props += " | UserComment=\(comment.prefix(40))" } } if let gps = dict[kCGImagePropertyGPSDictionary as String] as? [String: Any] { props += " | GPS keys=\(gps.keys.count)" } } if let image = CGImageSourceCreateImageAtIndex(source, 0, nil) { decoded = true width = image.width height = image.height } if wantThumbnail { let opts: [CFString: Any] = [ kCGImageSourceCreateThumbnailFromImageAlways: true, kCGImageSourceThumbnailMaxPixelSize: 256, ] thumbOK = CGImageSourceCreateThumbnailAtIndex(source, 0, opts as CFDictionary) != nil } return ImageResult(frameCount: count, decoded: decoded, width: width, height: height, properties: props, thumbnail: thumbOK) } // ---- libarchive enum Archive { typealias NewFn = @convention(c) () -> UnsafeMutableRawPointer? typealias Int1Fn = @convention(c) (UnsafeMutableRawPointer?) -> Int32 typealias OpenMemFn = @convention(c) (UnsafeMutableRawPointer?, UnsafeRawPointer?, Int) -> Int32 typealias NextFn = @convention(c) (UnsafeMutableRawPointer?, UnsafeMutablePointer?) -> Int32 typealias PathFn = @convention(c) (UnsafeMutableRawPointer?) -> UnsafePointer? typealias SizeFn = @convention(c) (UnsafeMutableRawPointer?) -> Int64 typealias FreeFn = @convention(c) (UnsafeMutableRawPointer?) -> Int32 typealias ExtractFn = @convention(c) (UnsafeMutableRawPointer?, UnsafeMutableRawPointer?, Int32) -> Int32 struct Bindings { let new: NewFn let supportFilterAll: Int1Fn let supportFormatAll: Int1Fn let openMemory: OpenMemFn let nextHeader: NextFn let pathname: PathFn let size: SizeFn let free: FreeFn let extract: ExtractFn } static func bind() -> Bindings? { guard let h = DyLib.shared.handle("/usr/lib/libarchive.2.dylib") else { return nil } guard let s1 = dlsym(h, "archive_read_new"), let s2 = dlsym(h, "archive_read_support_filter_all"), let s3 = dlsym(h, "archive_read_support_format_all"), let s4 = dlsym(h, "archive_read_open_memory"), let s5 = dlsym(h, "archive_read_next_header"), let s6 = dlsym(h, "archive_entry_pathname"), let s7 = dlsym(h, "archive_entry_size"), let s8 = dlsym(h, "archive_read_free"), let s9 = dlsym(h, "archive_read_extract") else { return nil } return Bindings( new: unsafeBitCast(s1, to: NewFn.self), supportFilterAll: unsafeBitCast(s2, to: Int1Fn.self), supportFormatAll: unsafeBitCast(s3, to: Int1Fn.self), openMemory: unsafeBitCast(s4, to: OpenMemFn.self), nextHeader: unsafeBitCast(s5, to: NextFn.self), pathname: unsafeBitCast(s6, to: PathFn.self), size: unsafeBitCast(s7, to: SizeFn.self), free: unsafeBitCast(s8, to: FreeFn.self), extract: unsafeBitCast(s9, to: ExtractFn.self)) } static func list(_ data: Data) throws -> (entries: [String], status: String) { guard let b = bind() else { throw LabError(message: tl("libarchive.2.dylib 无法 dlopen(沙盒或路径受限)")) } guard let handle = b.new() else { throw LabError(message: tl("archive_read_new 失败")) } defer { _ = b.free(handle) } _ = b.supportFilterAll(handle) _ = b.supportFormatAll(handle) let rc = data.withUnsafeBytes { raw -> Int32 in guard let base = raw.baseAddress else { return -1 } return b.openMemory(handle, base, data.count) } guard rc == 0 else { throw LabError(message: "archive_read_open_memory rc=\(rc)") } var entries: [String] = [] var status = "" while true { var entry: UnsafeMutableRawPointer? let r = b.nextHeader(handle, &entry) if r == 0, let e = entry { let name = b.pathname(e).map { String(cString: $0) } ?? "" entries.append("\(name) (\(b.size(e)) B)") if entries.count > 64 { break } } else if r == 1 { status = "EOF" break } else { status = "next_header rc=\(r)" break } } return (entries, status) } /// Extract every entry into `destination`. Uses archive_read_extract so IPA, DEB, /// tar and zip all unpack through the same path — no external binary needed. static func extractAll(_ data: Data, to destination: URL) -> (count: Int, errors: Int, status: String) { let fm = FileManager.default try? fm.createDirectory(at: destination, withIntermediateDirectories: true) guard let b = bind() else { return (0, 0, tl("libarchive 不可用")) } guard let handle = b.new() else { return (0, 0, tl("archive_read_new 失败")) } defer { _ = b.free(handle) } _ = b.supportFilterAll(handle) _ = b.supportFormatAll(handle) let rc = data.withUnsafeBytes { raw -> Int32 in guard let base = raw.baseAddress else { return -1 } return b.openMemory(handle, base, data.count) } guard rc == 0 else { return (0, 0, "open_memory rc=\(rc)") } let previous = fm.currentDirectoryPath fm.changeCurrentDirectoryPath(destination.path) defer { fm.changeCurrentDirectoryPath(previous) } var count = 0 var errors = 0 while true { var entry: UnsafeMutableRawPointer? let r = b.nextHeader(handle, &entry) if r != 0 { break } guard let e = entry else { break } // ARCHIVE_EXTRACT_TIME | PERM | SECURE_SYMLINKS | SECURE_NODOTDOT. // NOT NO_AUTODIR (0x0400) — archives without explicit directory entries // need the parents created for them. let flags: Int32 = 0x0004 | 0x0002 | 0x0100 | 0x0200 if b.extract(handle, e, flags) == 0 { count += 1 } else { errors += 1 } if count + errors > 20000 { break } } return (count, errors, tl("完成")) } } // ---- CoreMedia Dolby Vision RPU parser static func rpuParse(_ payload: Data, version: inout UInt32, level: inout UInt32) throws -> Int32 { guard let sym = DyLib.shared.symbol("CMGetDMVersionLevelFromRPU", in: [ "/System/Library/Frameworks/CoreMedia.framework/CoreMedia" ]) else { throw LabError(message: tl("CMGetDMVersionLevelFromRPU 未导出")) } typealias Fn = @convention(c) (UnsafePointer?, Int, UnsafeMutablePointer?, UnsafeMutablePointer?) -> Int32 let fn = unsafeBitCast(sym, to: Fn.self) var v: UInt32 = 0 var l: UInt32 = 0 let rc: Int32 = payload.withUnsafeBytes { raw in guard let base = raw.baseAddress?.assumingMemoryBound(to: UInt8.self) else { return -1 } return fn(base, payload.count, &v, &l) } version = v level = l return rc } // ---- AppleFSCompression (decmpfs) round trip struct DecmpfsResult { var wroteXattr: Bool var readBack: Int var kernelPointerHits: Int var samples: [String] var note: String } static func decmpfsProbe(at url: URL, uncompressed: Int, chunks: Int, producePerChunk: Int) -> DecmpfsResult { let fm = FileManager.default try? fm.removeItem(at: url) fm.createFile(atPath: url.path, contents: Data(repeating: 0x41, count: max(64, uncompressed))) let payload = Decmpfs.chunkedXattr(uncompressedSize: uncompressed, chunkCount: chunks, producePerChunk: producePerChunk) let ok = payload.withUnsafeBytes { raw -> Bool in guard let base = raw.baseAddress else { return false } return setxattr(url.path, "com.apple.decmpfs", base, payload.count, 0, 0) == 0 } if !ok { return DecmpfsResult(wroteXattr: false, readBack: 0, kernelPointerHits: 0, samples: [], note: tl("setxattr 失败 errno=") + "\(errno)") } guard let back = try? Data(contentsOf: url) else { return DecmpfsResult(wroteXattr: true, readBack: 0, kernelPointerHits: 0, samples: [], note: tl("读取失败 errno=") + "\(errno)") } // scan for kernel-space pointers in whatever the kernel handed back var hits = 0 var samples: [String] = [] let bytes = [UInt8](back) if bytes.count >= 8 { var i = 0 while i + 8 <= bytes.count { var v: UInt64 = 0 for k in 0..<8 { v |= UInt64(bytes[i + k]) << (8 * UInt64(k)) } if v >= 0xFFFF_FFE0_0000_0000 { hits += 1 if samples.count < 8 { samples.append(String(format: "0x%016llx @ +0x%x", v, i)) } } i += 8 } } return DecmpfsResult(wroteXattr: true, readBack: back.count, kernelPointerHits: hits, samples: samples, note: tl("读取 ") + "\(back.count)" + tl(" 字节")) } // ---- FontParser static func loadFont(_ data: Data) throws -> String { guard let provider = CGDataProvider(data: data as CFData) else { throw LabError(message: tl("CGDataProvider 创建失败")) } guard let font = CGFont(provider) else { throw LabError(message: tl("CGFont 拒绝该字体数据")) } let name = font.fullName as String? ?? font.postScriptName as String? ?? "" let glyphCount = font.numberOfGlyphs let ctFont = CTFontCreateWithGraphicsFont(font, 96, nil, nil) let pixels = 256 let bytesPerRow = pixels * 4 let buffer = UnsafeMutableRawPointer.allocate(byteCount: bytesPerRow * pixels, alignment: 16) defer { buffer.deallocate() } guard let ctx = CGContext(data: buffer, width: pixels, height: pixels, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { throw LabError(message: tl("上下文创建失败")) } // drawing forces the charstring interpreter to run for g in 1.. String { let start = CFAbsoluteTimeGetCurrent() let asset = MDLAsset(url: url) let ms = Int((CFAbsoluteTimeGetCurrent() - start) * 1000) return tl("MDLAsset 载入完成 ") + "\(ms)" + tl(" ms,对象数 ") + "\(asset.count)" } // ---- CoreHaptics / AVHaptic static func hapticCustomAudio(frameCount: Int, declaredBytes: Int) -> String { var report: [String] = [] let engine: CHHapticEngine do { engine = try CHHapticEngine() } catch { return tl("CHHapticEngine 初始化失败: ") + "\(error.localizedDescription)" } do { try engine.start() report.append(tl("CHHapticEngine 启动成功")) } catch { return tl("引擎启动失败: ") + "\(error.localizedDescription)" } // Build a mono 16-bit WAV with exactly `frameCount` samples and register it as an // audio resource. The declared byte count is what the server is asked to trust. let sampleRate = 48000 let frames = max(1, frameCount) var wav: [UInt8] = [] wav += B.ascii("RIFF") wav += B.le32(UInt32(36 + frames * 2)) wav += B.ascii("WAVEfmt ") wav += B.le32(16) + B.le16(1) + B.le16(1) wav += B.le32(UInt32(sampleRate)) + B.le32(UInt32(sampleRate * 2)) wav += B.le16(2) + B.le16(16) wav += B.ascii("data") + B.le32(UInt32(frames * 2)) wav += [UInt8](repeating: 0, count: frames * 2) let url = FileManager.default.temporaryDirectory.appendingPathComponent("cinder_haptic.wav") try? Data(wav).write(to: url) var resourceID: CHHapticAudioResourceID = 0 do { resourceID = try engine.registerAudioResource(url, options: [AnyHashable: Any]()) } catch { report.append(tl("registerAudioResource 失败: ") + "\(error.localizedDescription)") engine.stop() return report.joined(separator: "\n") } report.append(tl("音频资源 id=") + "\(resourceID)" + tl(" frames=") + "\(frames)" + tl(" declaredBytes=") + "\(declaredBytes)") do { let event = CHHapticEvent(audioResourceID: resourceID, parameters: [], relativeTime: 0, duration: 0.25) let pattern = try CHHapticPattern(events: [event], parameters: []) let player = try engine.makePlayer(with: pattern) try player.start(atTime: 0) report.append(tl("audioCustom 事件已投递")) Thread.sleep(forTimeInterval: 0.4) engine.stop() report.append(tl("引擎已停止")) } catch { report.append(tl("事件投递失败: ") + "\(error.localizedDescription)") engine.stop() } return report.joined(separator: "\n") } // ---- AudioCodecs probes static func audioConverter(format: UInt32, sampleRate: Double, channels: UInt32) -> String { var inFormat = AudioStreamBasicDescription(mSampleRate: sampleRate, mFormatID: format, mFormatFlags: 0, mBytesPerPacket: 0, mFramesPerPacket: 1, mBytesPerFrame: 0, mChannelsPerFrame: channels, mBitsPerChannel: 0, mReserved: 0) var outFormat = AudioStreamBasicDescription(mSampleRate: sampleRate, mFormatID: kAudioFormatLinearPCM, mFormatFlags: kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked, mBytesPerPacket: 4, mFramesPerPacket: 1, mBytesPerFrame: 4, mChannelsPerFrame: 1, mBitsPerChannel: 32, mReserved: 0) var converter: AudioConverterRef? let status = AudioConverterNew(&inFormat, &outFormat, &converter) if let c = converter { AudioConverterDispose(c) } return "AudioConverterNew(\(fourCC(format))) = \(status)" } static func fourCC(_ v: UInt32) -> String { let bytes = [UInt8((v >> 24) & 0xff), UInt8((v >> 16) & 0xff), UInt8((v >> 8) & 0xff), UInt8(v & 0xff)] let s = bytes.map { ($0 >= 0x20 && $0 < 0x7f) ? String(UnicodeScalar($0)) : "?" }.joined() return "'\(s)'" } // ---- IOKit / private framework probes static func dlopenProbe(_ path: String, symbols: [String]) -> String { guard let handle = dlopen(path, RTLD_NOW) else { let err = dlerror().map { String(cString: $0) } ?? tl("dlopen 失败") return "✗ \(path)\n \(err)" } var found: [String] = [] var missing: [String] = [] for s in symbols { if dlsym(handle, s) != nil { found.append(s) } else { missing.append(s) } } var out = "✓ \(path)" if !found.isEmpty { out += "\n resolved: " + found.joined(separator: ", ") } if !missing.isEmpty { out += "\n missing: " + missing.joined(separator: ", ") } return out } }