import AppKit
|
import ApplicationServices
|
import CryptoKit
|
import Darwin
|
import Foundation
|
|
func validDrag(start: CGPoint, end: CGPoint, displays: [CGRect]) -> Bool {
|
let values = [start.x, start.y, end.x, end.y]
|
guard values.allSatisfy(\.isFinite), start != end, !displays.isEmpty else { return false }
|
return displays.contains(where: { $0.contains(start) }) && displays.contains(where: { $0.contains(end) })
|
}
|
|
struct ReceiverIdentity: Equatable {
|
let pid: Int32
|
let windowID: UInt32
|
}
|
|
struct WindowSnapshot {
|
let identity: ReceiverIdentity
|
let layer: Int
|
let bounds: CGRect
|
let alpha: Double
|
let onScreen: Bool
|
}
|
|
struct SourceIdentity: Equatable {
|
let byteCount: Int64
|
let digest: String
|
let device: UInt64
|
let inode: UInt64
|
|
init(byteCount: Int64, digest: String, device: UInt64 = 0, inode: UInt64 = 0) {
|
self.byteCount = byteCount
|
self.digest = digest
|
self.device = device
|
self.inode = inode
|
}
|
}
|
|
func validRect(_ rect: CGRect) -> Bool {
|
[rect.origin.x, rect.origin.y, rect.size.width, rect.size.height].allSatisfy(\.isFinite)
|
&& rect.size.width > 0 && rect.size.height > 0
|
}
|
|
func topReceiver(at point: CGPoint, expectedPID: Int32, windows: [WindowSnapshot]) -> ReceiverIdentity? {
|
guard expectedPID > 0, !windows.isEmpty,
|
windows.allSatisfy({ snapshot in
|
snapshot.identity.pid > 0 && snapshot.identity.windowID > 0
|
&& snapshot.alpha.isFinite && snapshot.alpha >= 0 && snapshot.alpha <= 1
|
&& validRect(snapshot.bounds)
|
}) else { return nil }
|
guard let top = windows.first(where: { $0.onScreen && $0.alpha > 0 && $0.bounds.contains(point) }),
|
top.layer == 0, top.identity.pid == expectedPID else { return nil }
|
return top.identity
|
}
|
|
func receiverStable(preflight: ReceiverIdentity, beforeMouseUp: ReceiverIdentity?) -> Bool {
|
preflight == beforeMouseUp
|
}
|
|
func requiredDragFlags() -> CGEventFlags { [.maskAlternate] }
|
|
func validQASourceScope(
|
rootComponents: [String],
|
sourceComponents: [String],
|
rootIsDirectory: Bool,
|
rootIsSymlink: Bool,
|
sourceExists: Bool,
|
sourceIsRegular: Bool,
|
sourceIsSymlink: Bool,
|
sourceSize: Int64,
|
maximumSize: Int64
|
) -> Bool {
|
guard !rootComponents.isEmpty, sourceComponents.count > rootComponents.count,
|
Array(sourceComponents.prefix(rootComponents.count)) == rootComponents,
|
rootIsDirectory, !rootIsSymlink, sourceExists, sourceIsRegular, !sourceIsSymlink,
|
sourceSize >= 0, maximumSize > 0, sourceSize <= maximumSize else { return false }
|
return true
|
}
|
|
func sourcePreserved(before: SourceIdentity, after: SourceIdentity?) -> Bool { before == after }
|
|
enum SourceCleanupResult: Equatable {
|
case preserved
|
case recovered
|
case failed
|
}
|
|
func sourceCleanupResult(
|
before: SourceIdentity,
|
after: SourceIdentity?,
|
restore: () -> Bool
|
) -> SourceCleanupResult {
|
if sourcePreserved(before: before, after: after) { return .preserved }
|
return restore() ? .recovered : .failed
|
}
|
|
func makeCopyMouseEvent(source: CGEventSource?, type: CGEventType, point: CGPoint) -> CGEvent? {
|
guard let event = CGEvent(
|
mouseEventSource: source,
|
mouseType: type,
|
mouseCursorPosition: point,
|
mouseButton: .left
|
) else { return nil }
|
event.flags = requiredDragFlags()
|
return event
|
}
|
|
func runSafeCancel(
|
escapeDown: () -> Bool,
|
escapeUp: () -> Bool,
|
mouseUp: () -> Bool
|
) -> Bool {
|
let down = escapeDown()
|
let up = escapeUp()
|
let released = mouseUp()
|
return down && up && released
|
}
|
|
struct CleanupState {
|
var mouseDownPosted = false
|
var mouseUpPosted = false
|
var cleanupAttempted = false
|
var cleanupSucceeded = false
|
|
mutating func failAfterMouseDown(cleanup: () -> Bool) -> Int32 {
|
guard mouseDownPosted && !mouseUpPosted else { return 70 }
|
cleanupAttempted = true
|
cleanupSucceeded = cleanup()
|
if cleanupSucceeded { mouseUpPosted = true }
|
return cleanupSucceeded ? 70 : 71
|
}
|
}
|
|
func selfTest() -> Int32 {
|
let bounds = [CGRect(x: 0, y: 0, width: 1920, height: 1080)]
|
let destination = CGPoint(x: 500, y: 500)
|
let candidate = ReceiverIdentity(pid: 42, windowID: 7)
|
let candidateWindow = WindowSnapshot(identity: candidate, layer: 0, bounds: CGRect(x: 400, y: 400, width: 300, height: 300), alpha: 1, onScreen: true)
|
let otherWindow = WindowSnapshot(identity: ReceiverIdentity(pid: 99, windowID: 8), layer: 0, bounds: candidateWindow.bounds, alpha: 1, onScreen: true)
|
let nonNormalWindow = WindowSnapshot(identity: ReceiverIdentity(pid: 42, windowID: 9), layer: 1, bounds: candidateWindow.bounds, alpha: 1, onScreen: true)
|
let invalidWindow = WindowSnapshot(identity: candidate, layer: 0, bounds: .zero, alpha: 1, onScreen: true)
|
let exactReceiver = topReceiver(at: destination, expectedPID: 42, windows: [candidateWindow])
|
let rootComponents = ["QA_ROOT"]
|
let sourceComponents = ["QA_ROOT", "SOURCE"]
|
let validSource = validQASourceScope(
|
rootComponents: rootComponents,
|
sourceComponents: sourceComponents,
|
rootIsDirectory: true,
|
rootIsSymlink: false,
|
sourceExists: true,
|
sourceIsRegular: true,
|
sourceIsSymlink: false,
|
sourceSize: 1024,
|
maximumSize: 16 * 1024 * 1024
|
)
|
let sourceBefore = SourceIdentity(byteCount: 1024, digest: "FIXTURE_DIGEST")
|
let builtCopyEvents = [CGEventType.leftMouseDown, .leftMouseDragged, .leftMouseUp].compactMap {
|
makeCopyMouseEvent(source: nil, type: $0, point: CGPoint(x: 1, y: 1))
|
}
|
var cancelCalls: [String] = []
|
let cancelComplete = runSafeCancel(
|
escapeDown: { cancelCalls.append("escape-down"); return true },
|
escapeUp: { cancelCalls.append("escape-up"); return true },
|
mouseUp: { cancelCalls.append("mouse-up"); return true }
|
)
|
var failedCancelCalls: [String] = []
|
let cancelIncomplete = runSafeCancel(
|
escapeDown: { failedCancelCalls.append("escape-down"); return false },
|
escapeUp: { failedCancelCalls.append("escape-up"); return true },
|
mouseUp: { failedCancelCalls.append("mouse-up"); return false }
|
)
|
guard validDrag(start: CGPoint(x: 1, y: 1), end: CGPoint(x: 2, y: 2), displays: bounds),
|
!validDrag(start: CGPoint(x: CGFloat.nan, y: 1), end: CGPoint(x: 2, y: 2), displays: bounds),
|
!validDrag(start: CGPoint(x: 1, y: 1), end: CGPoint(x: CGFloat.infinity, y: 2), displays: bounds),
|
!validDrag(start: CGPoint(x: 1, y: 1), end: CGPoint(x: 1, y: 1), displays: bounds),
|
!validDrag(start: CGPoint(x: -1, y: 1), end: CGPoint(x: 2, y: 2), displays: bounds),
|
exactReceiver == candidate,
|
topReceiver(at: destination, expectedPID: 42, windows: [otherWindow, candidateWindow]) == nil,
|
topReceiver(at: destination, expectedPID: 42, windows: []) == nil,
|
topReceiver(at: destination, expectedPID: 42, windows: [invalidWindow]) == nil,
|
topReceiver(at: destination, expectedPID: 42, windows: [nonNormalWindow, candidateWindow]) == nil,
|
topReceiver(at: destination, expectedPID: 7, windows: [candidateWindow]) == nil,
|
receiverStable(preflight: candidate, beforeMouseUp: candidate),
|
!receiverStable(preflight: candidate, beforeMouseUp: ReceiverIdentity(pid: 42, windowID: 10)),
|
requiredDragFlags().contains(.maskAlternate),
|
builtCopyEvents.count == 3,
|
builtCopyEvents.allSatisfy({ $0.flags.contains(.maskAlternate) }),
|
validSource,
|
!validQASourceScope(rootComponents: rootComponents, sourceComponents: ["OUTSIDE", "SOURCE"], rootIsDirectory: true, rootIsSymlink: false, sourceExists: true, sourceIsRegular: true, sourceIsSymlink: false, sourceSize: 1, maximumSize: 16),
|
!validQASourceScope(rootComponents: rootComponents, sourceComponents: sourceComponents, rootIsDirectory: true, rootIsSymlink: false, sourceExists: true, sourceIsRegular: true, sourceIsSymlink: true, sourceSize: 1, maximumSize: 16),
|
!validQASourceScope(rootComponents: rootComponents, sourceComponents: sourceComponents, rootIsDirectory: true, rootIsSymlink: false, sourceExists: false, sourceIsRegular: true, sourceIsSymlink: false, sourceSize: 1, maximumSize: 16),
|
!validQASourceScope(rootComponents: rootComponents, sourceComponents: sourceComponents, rootIsDirectory: true, rootIsSymlink: false, sourceExists: true, sourceIsRegular: false, sourceIsSymlink: false, sourceSize: 1, maximumSize: 16),
|
!validQASourceScope(rootComponents: rootComponents, sourceComponents: sourceComponents, rootIsDirectory: true, rootIsSymlink: false, sourceExists: true, sourceIsRegular: true, sourceIsSymlink: false, sourceSize: 17, maximumSize: 16),
|
sourcePreserved(before: sourceBefore, after: sourceBefore),
|
!sourcePreserved(before: sourceBefore, after: nil),
|
!sourcePreserved(before: sourceBefore, after: SourceIdentity(byteCount: 1024, digest: "CHANGED")),
|
sourceCleanupResult(before: sourceBefore, after: sourceBefore, restore: { false }) == .preserved,
|
sourceCleanupResult(before: sourceBefore, after: nil, restore: { true }) == .recovered,
|
sourceCleanupResult(before: sourceBefore, after: nil, restore: { false }) == .failed,
|
cancelComplete, cancelCalls == ["escape-down", "escape-up", "mouse-up"],
|
!cancelIncomplete, failedCancelCalls == ["escape-down", "escape-up", "mouse-up"] else { return 1 }
|
var recovered = CleanupState(mouseDownPosted: true)
|
guard recovered.failAfterMouseDown(cleanup: { true }) == 70,
|
recovered.cleanupAttempted, recovered.cleanupSucceeded, recovered.mouseUpPosted else { return 2 }
|
var failed = CleanupState(mouseDownPosted: true)
|
guard failed.failAfterMouseDown(cleanup: { false }) == 71,
|
failed.cleanupAttempted, !failed.cleanupSucceeded, !failed.mouseUpPosted else { return 3 }
|
print("self_test=PASS coordinate_validation=true mouse_up_cleanup=true")
|
return 0
|
}
|
|
if CommandLine.arguments == [CommandLine.arguments[0], "--self-test"] {
|
exit(selfTest())
|
}
|
|
guard CommandLine.arguments.count == 8,
|
let startX = Double(CommandLine.arguments[1]),
|
let startY = Double(CommandLine.arguments[2]),
|
let endX = Double(CommandLine.arguments[3]),
|
let endY = Double(CommandLine.arguments[4]),
|
let candidatePID = Int32(CommandLine.arguments[5]), candidatePID > 0,
|
!CommandLine.arguments[6].isEmpty,
|
!CommandLine.arguments[7].isEmpty else {
|
fputs("usage: NativeDrag startX startY endX endY candidatePID qaRoot qaSource\n", stderr)
|
exit(64)
|
}
|
|
let maximumSourceBytes: Int64 = 16 * 1024 * 1024
|
let rootInputURL = URL(fileURLWithPath: CommandLine.arguments[6]).standardizedFileURL
|
let sourceInputURL = URL(fileURLWithPath: CommandLine.arguments[7]).standardizedFileURL
|
let rootURL = rootInputURL.resolvingSymlinksInPath()
|
let sourceURL = sourceInputURL.resolvingSymlinksInPath()
|
|
func fileStatus(_ url: URL) -> stat? {
|
var value = stat()
|
return lstat(url.path, &value) == 0 ? value : nil
|
}
|
func isMode(_ status: stat, _ expected: mode_t) -> Bool {
|
status.st_mode & mode_t(S_IFMT) == expected
|
}
|
func digest(_ data: Data) -> String {
|
SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined()
|
}
|
func sourceIdentity(_ url: URL) -> SourceIdentity? {
|
guard let status = fileStatus(url), isMode(status, mode_t(S_IFREG)),
|
!isMode(status, mode_t(S_IFLNK)), status.st_size >= 0,
|
status.st_size <= maximumSourceBytes,
|
let data = try? Data(contentsOf: url, options: .mappedIfSafe),
|
data.count == Int(status.st_size) else { return nil }
|
return SourceIdentity(
|
byteCount: status.st_size,
|
digest: digest(data),
|
device: UInt64(status.st_dev),
|
inode: UInt64(status.st_ino)
|
)
|
}
|
func sourceScopeValid() -> Bool {
|
guard let rootStatus = fileStatus(rootInputURL), let sourceStatus = fileStatus(sourceInputURL) else { return false }
|
return validQASourceScope(
|
rootComponents: rootURL.pathComponents,
|
sourceComponents: sourceURL.pathComponents,
|
rootIsDirectory: isMode(rootStatus, mode_t(S_IFDIR)),
|
rootIsSymlink: isMode(rootStatus, mode_t(S_IFLNK)),
|
sourceExists: true,
|
sourceIsRegular: isMode(sourceStatus, mode_t(S_IFREG)),
|
sourceIsSymlink: isMode(sourceStatus, mode_t(S_IFLNK)),
|
sourceSize: sourceStatus.st_size,
|
maximumSize: maximumSourceBytes
|
)
|
}
|
guard sourceScopeValid(), let sourceBefore = sourceIdentity(sourceURL),
|
let sourcePreimage = try? Data(contentsOf: sourceURL),
|
sourcePreimage.count == Int(sourceBefore.byteCount) else {
|
fputs("qa source scope failed closed\n", stderr)
|
exit(64)
|
}
|
|
func restoreSourcePreimage() -> Bool {
|
let currentURL = sourceInputURL.resolvingSymlinksInPath()
|
guard Array(currentURL.pathComponents.prefix(rootURL.pathComponents.count)) == rootURL.pathComponents,
|
currentURL.pathComponents.count > rootURL.pathComponents.count else { return false }
|
if let currentStatus = fileStatus(sourceInputURL) {
|
guard isMode(currentStatus, mode_t(S_IFREG)), !isMode(currentStatus, mode_t(S_IFLNK)) else { return false }
|
}
|
do {
|
try sourcePreimage.write(to: sourceInputURL, options: .atomic)
|
guard let restored = sourceIdentity(sourceInputURL) else { return false }
|
return restored.byteCount == sourceBefore.byteCount && restored.digest == sourceBefore.digest
|
} catch {
|
return false
|
}
|
}
|
|
func windowSnapshots() -> [WindowSnapshot]? {
|
let options: CGWindowListOption = [.optionOnScreenOnly, .excludeDesktopElements]
|
guard let raw = CGWindowListCopyWindowInfo(options, kCGNullWindowID) as? [[String: Any]], !raw.isEmpty else { return nil }
|
var result: [WindowSnapshot] = []
|
for item in raw {
|
guard let pid = (item[kCGWindowOwnerPID as String] as? NSNumber)?.int32Value,
|
let windowID = (item[kCGWindowNumber as String] as? NSNumber)?.uint32Value,
|
let layer = (item[kCGWindowLayer as String] as? NSNumber)?.intValue,
|
let alpha = (item[kCGWindowAlpha as String] as? NSNumber)?.doubleValue,
|
let onScreen = (item[kCGWindowIsOnscreen as String] as? NSNumber)?.boolValue,
|
let boundsValue = item[kCGWindowBounds as String],
|
CFGetTypeID(boundsValue as CFTypeRef) == CFDictionaryGetTypeID(),
|
let bounds = CGRect(dictionaryRepresentation: boundsValue as! CFDictionary) else { return nil }
|
result.append(WindowSnapshot(
|
identity: ReceiverIdentity(pid: pid, windowID: windowID),
|
layer: layer,
|
bounds: bounds,
|
alpha: alpha,
|
onScreen: onScreen
|
))
|
}
|
return result
|
}
|
|
var displayCount: UInt32 = 0
|
guard CGGetActiveDisplayList(0, nil, &displayCount) == .success, displayCount > 0 else { exit(65) }
|
var displayIDs = [CGDirectDisplayID](repeating: 0, count: Int(displayCount))
|
guard CGGetActiveDisplayList(displayCount, &displayIDs, &displayCount) == .success else { exit(65) }
|
let frozenDisplayBounds = displayIDs.prefix(Int(displayCount)).map(CGDisplayBounds)
|
let start = CGPoint(x: startX, y: startY)
|
let end = CGPoint(x: endX, y: endY)
|
guard validDrag(start: start, end: end, displays: frozenDisplayBounds) else {
|
fputs("invalid or out-of-bounds drag coordinates\n", stderr)
|
exit(64)
|
}
|
|
let finderApplications = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.finder")
|
guard NSRunningApplication(processIdentifier: candidatePID) != nil,
|
finderApplications.count == 1, let finder = finderApplications.first,
|
let preflightWindows = windowSnapshots(),
|
topReceiver(at: start, expectedPID: finder.processIdentifier, windows: preflightWindows) != nil,
|
let destinationReceiver = topReceiver(at: end, expectedPID: candidatePID, windows: preflightWindows) else {
|
fputs("delivery receiver preflight failed closed\n", stderr)
|
exit(65)
|
}
|
|
let trusted = AXIsProcessTrusted()
|
let postAllowed = CGPreflightPostEventAccess()
|
print("AXIsProcessTrusted=\(trusted)")
|
print("CGPreflightPostEventAccess=\(postAllowed)")
|
guard trusted && postAllowed else { exit(77) }
|
|
let source = CGEventSource(stateID: .hidSystemState)
|
func postMouse(_ type: CGEventType, at point: CGPoint) -> Bool {
|
guard let event = makeCopyMouseEvent(source: source, type: type, point: point) else { return false }
|
event.post(tap: .cghidEventTap)
|
return true
|
}
|
func postEscape(_ keyDown: Bool) -> Bool {
|
guard let event = CGEvent(keyboardEventSource: source, virtualKey: 53, keyDown: keyDown) else { return false }
|
event.post(tap: .cghidEventTap)
|
return true
|
}
|
func cancelDrag() -> Bool {
|
runSafeCancel(
|
escapeDown: { postEscape(true) },
|
escapeUp: { postEscape(false) },
|
mouseUp: { postMouse(.leftMouseUp, at: start) }
|
)
|
}
|
|
func terminateFailure(receiverIdentityPassed: Bool, cleanupComplete: Bool) -> Never {
|
let sourceResult = sourceCleanupResult(
|
before: sourceBefore,
|
after: sourceIdentity(sourceInputURL),
|
restore: restoreSourcePreimage
|
)
|
let sourceWasPreserved = sourceResult == .preserved
|
let complete = cleanupComplete && sourceResult != .failed
|
fputs(
|
"receiver_identity=\(receiverIdentityPassed ? "PASS" : "FAIL") copy_semantic=PASS source_preserved=\(sourceWasPreserved ? "PASS" : "FAIL") cleanup=\(complete ? "PASS" : "FAIL")\n",
|
stderr
|
)
|
exit(complete ? 70 : 71)
|
}
|
|
var lastPoint = start
|
guard postMouse(.mouseMoved, at: start) else { exit(70) }
|
Thread.sleep(forTimeInterval: 0.35)
|
guard postMouse(.leftMouseDown, at: start) else { exit(70) }
|
Thread.sleep(forTimeInterval: 0.45)
|
|
let steps = 36
|
for index in 1...steps {
|
let progress = Double(index) / Double(steps)
|
let eased = progress * progress * (3.0 - 2.0 * progress)
|
lastPoint = CGPoint(x: start.x + (end.x - start.x) * eased, y: start.y + (end.y - start.y) * eased)
|
guard postMouse(.leftMouseDragged, at: lastPoint) else {
|
terminateFailure(receiverIdentityPassed: false, cleanupComplete: cancelDrag())
|
}
|
Thread.sleep(forTimeInterval: 0.035)
|
}
|
|
Thread.sleep(forTimeInterval: 0.75)
|
let receiverBeforeMouseUp = windowSnapshots().flatMap {
|
topReceiver(at: end, expectedPID: candidatePID, windows: $0)
|
}
|
guard receiverStable(preflight: destinationReceiver, beforeMouseUp: receiverBeforeMouseUp) else {
|
terminateFailure(receiverIdentityPassed: false, cleanupComplete: cancelDrag())
|
}
|
guard postMouse(.leftMouseUp, at: end) else {
|
terminateFailure(receiverIdentityPassed: true, cleanupComplete: cancelDrag())
|
}
|
Thread.sleep(forTimeInterval: 0.5)
|
guard sourcePreserved(before: sourceBefore, after: sourceIdentity(sourceInputURL)) else {
|
terminateFailure(receiverIdentityPassed: true, cleanupComplete: true)
|
}
|
print("receiver_identity=PASS copy_semantic=PASS source_preserved=PASS cleanup=PASS")
|