Files
ghostgram/submodules/TelegramUI/Components/RichTextEditor/Sources/RichTextInputDecUpstream/PrivateUIKit/IDPrivateUIKit.m
T
ichmagmaus111andClaude Opus 5.5 534cabe35b bug fixes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 16:55:08 +01:00

3051 lines
137 KiB
Objective-C

#import <TargetConditionals.h>
#if TARGET_OS_IOS
#import "TGPrivateRuntimeNames.h"
#import "IDPrivateUIKit.h"
#import "IDPrivateUIKit+Testing.h"
// telegram-ios DEVIATION 5 (see PROVENANCE.md) — for the pristine contract manifest.
#import "RichTextInputDecUpstreamPristineManifest.h"
#import <QuartzCore/QuartzCore.h>
#import <objc/message.h>
#import <objc/runtime.h>
#import <string.h>
static const void *IDPrivateTextAutoscrollTraceKey = &IDPrivateTextAutoscrollTraceKey;
static const void *IDPrivateStockPresentationKey = &IDPrivateStockPresentationKey;
static const void *IDPrivateRecognizerBaselineKey = &IDPrivateRecognizerBaselineKey;
static const void *IDPrivateInteractionHostTopologyBaselineKey =
&IDPrivateInteractionHostTopologyBaselineKey;
static const void *IDPrivateFloatingContainerIdentityTrackerKey =
&IDPrivateFloatingContainerIdentityTrackerKey;
static const void *IDPrivateFloatingContainerIdentityTokenKey =
&IDPrivateFloatingContainerIdentityTokenKey;
static NSUInteger IDPrivateNextFloatingContainerIdentityToken;
static NSMutableArray<NSString *> *IDPrivateAutocorrectionTrace;
static NSDictionary<NSString *, NSNumber *> *IDPrivatePointJSON(CGPoint point);
void IDInstallAutocorrectionTraceForUITesting(void) {
@synchronized (IDPrivateUIKit.class) {
IDPrivateAutocorrectionTrace = [NSMutableArray array];
}
}
void IDRecordAutocorrectionTraceForUITesting(NSString *event) {
if (event.length == 0) return;
@synchronized (IDPrivateUIKit.class) {
[IDPrivateAutocorrectionTrace addObject:[event copy]];
}
}
NSArray<NSString *> *IDCopyAutocorrectionTraceForUITesting(void) {
@synchronized (IDPrivateUIKit.class) {
return IDPrivateAutocorrectionTrace.copy ?: @[];
}
}
void IDResetAutocorrectionTraceForUITesting(void) {
@synchronized (IDPrivateUIKit.class) {
IDPrivateAutocorrectionTrace = nil;
}
}
static BOOL IDPrivateMethodHasNoExplicitArgumentsAndReturnType(
Method method, const char *returnType) {
if (method == NULL || method_getNumberOfArguments(method) != 2) return NO;
char *actualReturnType = method_copyReturnType(method);
BOOL matches = actualReturnType != NULL &&
strcmp(actualReturnType, returnType) == 0;
free(actualReturnType);
return matches;
}
BOOL IDPrivateTestingDismissInlineTextCompletionWithKeyboardClass(
Class keyboardClass) {
if (keyboardClass == Nil) return NO;
SEL activeSelector = NSSelectorFromString(TGPN_activeInstance());
Method activeMethod = class_getClassMethod(keyboardClass, activeSelector);
if (!IDPrivateMethodHasNoExplicitArgumentsAndReturnType(
activeMethod, @encode(id))) {
return NO;
}
id keyboard = IDPrivateObject0(keyboardClass, activeSelector);
SEL removeSelector =
NSSelectorFromString(TGPN_removeInlineTextCompletionPrompt());
if (keyboard == nil) return NO;
Method removeMethod =
class_getInstanceMethod([keyboard class], removeSelector);
if (!IDPrivateMethodHasNoExplicitArgumentsAndReturnType(
removeMethod, @encode(void))) {
return NO;
}
IDPrivateVoid0(keyboard, removeSelector);
return YES;
}
BOOL IDPrivateDismissInlineTextCompletionForUITesting(void) {
return IDPrivateTestingDismissInlineTextCompletionWithKeyboardClass(
NSClassFromString(TGPN_UIKeyboardImpl()));
}
static NSArray<NSString *> *IDPrivateRecognizerIdentities(UIView *view) {
NSMutableArray<NSString *> *identities = [NSMutableArray array];
for (UIGestureRecognizer *recognizer in view.gestureRecognizers ?: @[]) {
[identities addObject:[NSString stringWithFormat:@"%p:%@", recognizer,
NSStringFromClass(recognizer.class)]];
}
[identities sortUsingSelector:@selector(compare:)];
return identities.copy;
}
static void IDPrivateFreezeRecognizerBaselineAtHeldCapture(UIView *view) {
objc_setAssociatedObject(view, IDPrivateRecognizerBaselineKey,
IDPrivateRecognizerIdentities(view), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
static NSDictionary<NSString *, id> *IDPrivateRecognizerState(UIView *view) {
NSArray<UIGestureRecognizer *> *recognizers = view.gestureRecognizers ?: @[];
NSArray<NSString *> *identities = IDPrivateRecognizerIdentities(view);
NSMutableArray<NSString *> *classes = [NSMutableArray array];
for (UIGestureRecognizer *recognizer in recognizers) {
[classes addObject:NSStringFromClass(recognizer.class)];
}
[classes sortUsingSelector:@selector(compare:)];
NSArray<NSString *> *baseline = objc_getAssociatedObject(view,
IDPrivateRecognizerBaselineKey);
if (baseline == nil) {
baseline = identities;
objc_setAssociatedObject(view, IDPrivateRecognizerBaselineKey, baseline,
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
NSMutableSet *unexpected = [NSMutableSet setWithArray:identities];
[unexpected minusSet:[NSSet setWithArray:baseline]];
return @{
@"recognizerCount": @(recognizers.count),
@"recognizerSignature": classes.copy,
@"recognizerBaselineCount": @(baseline.count),
@"recognizerUnexpectedNewCount": @(unexpected.count),
@"recognizerSubsetOfBaseline": @((BOOL)(unexpected.count == 0)),
@"recognizerBaselineIdentitySignature": baseline,
@"recognizerIdentitySignature": identities,
};
}
void IDPrivateResetRecognizerBaselineForUITesting(UIView *textInput) {
objc_setAssociatedObject(textInput, IDPrivateRecognizerBaselineKey, nil,
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
static NSString *IDPrivateTopologyObjectIdentity(id object) {
return [NSString stringWithFormat:@"%p:%@", object,
NSStringFromClass([object class])];
}
static NSString *IDPrivateTopologyViewToken(UIView *view) {
return view == nil ? @"nil" : [NSString stringWithFormat:@"%p", view];
}
static NSArray<NSString *> *IDPrivateTopologySortedDifference(
NSArray<NSString *> *left,
NSArray<NSString *> *right) {
NSMutableSet<NSString *> *difference = [NSMutableSet setWithArray:left];
[difference minusSet:[NSSet setWithArray:right]];
return [[difference allObjects] sortedArrayUsingSelector:@selector(compare:)];
}
static NSString *IDPrivateTopologyAncestry(
NSString *logicalName,
UIView *view,
UIView *outer) {
if (view == nil) return @"none";
if (view == outer) return @"outer";
if ([view isDescendantOfView:outer]) return @"descendant";
if ([logicalName isEqualToString:@"external"]) return @"external";
return @"unrelated";
}
static NSDictionary<NSString *, id> *IDPrivateTopologyRawHostState(
NSString *logicalName,
UIView *view,
UIView *outer) {
NSArray<UIGestureRecognizer *> *recognizers = view.gestureRecognizers ?: @[];
NSArray<id<UIInteraction>> *interactions = view.interactions ?: @[];
NSMutableArray<NSString *> *recognizerClasses = [NSMutableArray array];
NSMutableArray<NSString *> *recognizerIdentities = [NSMutableArray array];
NSMutableArray<NSString *> *recognizerEnabled = [NSMutableArray array];
NSMutableArray<NSString *> *recognizerOwners = [NSMutableArray array];
for (UIGestureRecognizer *recognizer in recognizers) {
NSString *identity = IDPrivateTopologyObjectIdentity(recognizer);
[recognizerClasses addObject:NSStringFromClass(recognizer.class)];
[recognizerIdentities addObject:identity];
[recognizerEnabled addObject:[NSString stringWithFormat:@"%@:%d",
identity, recognizer.enabled]];
[recognizerOwners addObject:IDPrivateTopologyViewToken(recognizer.view)];
}
NSMutableArray<NSString *> *interactionClasses = [NSMutableArray array];
NSMutableArray<NSString *> *interactionIdentities = [NSMutableArray array];
NSMutableArray<NSString *> *interactionOwners = [NSMutableArray array];
for (id<UIInteraction> interaction in interactions) {
[interactionClasses addObject:NSStringFromClass([interaction class])];
[interactionIdentities addObject:IDPrivateTopologyObjectIdentity(interaction)];
[interactionOwners addObject:IDPrivateTopologyViewToken(interaction.view)];
}
for (NSMutableArray<NSString *> *signature in @[
recognizerClasses, recognizerIdentities, recognizerEnabled,
recognizerOwners, interactionClasses, interactionIdentities,
interactionOwners]) {
[signature sortUsingSelector:@selector(compare:)];
}
return @{
@"present": @(view != nil),
@"viewToken": IDPrivateTopologyViewToken(view),
@"ancestry": IDPrivateTopologyAncestry(logicalName, view, outer),
@"recognizerCount": @(recognizers.count),
@"recognizerSignature": recognizerClasses.copy,
@"recognizerIdentitySignature": recognizerIdentities.copy,
@"recognizerEnabledSignature": recognizerEnabled.copy,
@"recognizerOwnerIdentitySignature": recognizerOwners.copy,
@"interactionCount": @(interactions.count),
@"interactionSignature": interactionClasses.copy,
@"interactionIdentitySignature": interactionIdentities.copy,
@"interactionOwnerIdentitySignature": interactionOwners.copy,
};
}
static NSUInteger IDPrivateTopologyEligibleRecognizerCount(
NSString *logicalName,
UIView *view,
UIView *outer) {
if (view == nil) return 0;
NSUInteger count = 0;
UIScrollView *outerScrollView = [outer isKindOfClass:UIScrollView.class]
? (UIScrollView *)outer : nil;
for (UIGestureRecognizer *recognizer in view.gestureRecognizers ?: @[]) {
if ([logicalName isEqualToString:@"outer"] &&
(recognizer == outerScrollView.panGestureRecognizer ||
recognizer == outerScrollView.pinchGestureRecognizer)) {
continue;
}
count += 1;
}
return count;
}
static NSString *IDPrivateTopologyInstallationHost(
NSDictionary<NSString *, NSDictionary<NSString *, id> *> *rawHosts,
NSDictionary<NSString *, UIView *> *views,
UIView *outer,
NSUInteger *liveHostCount) {
UIView *inner = views[@"blockStack"];
UIView *external = views[@"external"];
BOOL aliasesLiveHost =
(inner != nil && inner == outer &&
[rawHosts[@"blockStack"][@"interactionCount"] unsignedIntegerValue] > 0) ||
(external != nil && external == outer &&
[rawHosts[@"external"][@"interactionCount"] unsignedIntegerValue] > 0) ||
(inner != nil && external != nil && inner == external &&
[rawHosts[@"blockStack"][@"interactionCount"] unsignedIntegerValue] > 0);
if (aliasesLiveHost) {
if (liveHostCount != NULL) *liveHostCount = 2;
return @"multiple";
}
NSArray<NSString *> *alternateNames = @[@"blockStack", @"external"];
NSMutableArray<NSString *> *live = [NSMutableArray array];
for (NSString *name in alternateNames) {
if ([rawHosts[name][@"interactionCount"] unsignedIntegerValue] > 0) {
[live addObject:name];
}
}
if (live.count == 0 &&
[rawHosts[@"outer"][@"interactionCount"] unsignedIntegerValue] > 0) {
[live addObject:@"outer"];
}
if (live.count == 0) {
for (NSString *name in alternateNames) {
if (IDPrivateTopologyEligibleRecognizerCount(
name, views[name], outer) > 0) {
[live addObject:name];
}
}
if (live.count == 0 && IDPrivateTopologyEligibleRecognizerCount(
@"outer", views[@"outer"], outer) > 0) {
[live addObject:@"outer"];
}
}
if (liveHostCount != NULL) *liveHostCount = live.count;
if (live.count == 0) return @"none";
if (live.count > 1) return @"multiple";
return live.firstObject;
}
static NSUInteger IDPrivateTopologyDuplicateIdentityCount(
NSDictionary<NSString *, NSDictionary<NSString *, id> *> *hosts,
NSString *identityKey) {
NSMutableDictionary<NSString *, NSNumber *> *occurrences =
[NSMutableDictionary dictionary];
for (NSString *name in @[@"outer", @"blockStack", @"external"]) {
for (NSString *identity in hosts[name][identityKey]) {
occurrences[identity] = @([occurrences[identity] unsignedIntegerValue] + 1);
}
}
NSUInteger duplicateCount = 0;
for (NSNumber *count in occurrences.allValues) {
if (count.unsignedIntegerValue > 1) duplicateCount += 1;
}
return duplicateCount;
}
NSDictionary<NSString *, id> *
IDPrivateCopyInteractionHostTopologyForUITesting(
UIView *outer,
UIView *inner,
UIView *external) {
NSCParameterAssert(outer != nil);
NSDictionary<NSString *, UIView *> *views = @{
@"outer": outer,
@"blockStack": inner ?: (UIView *)NSNull.null,
@"external": external ?: (UIView *)NSNull.null,
};
NSMutableDictionary<NSString *, NSDictionary<NSString *, id> *> *rawHosts =
[NSMutableDictionary dictionary];
for (NSString *name in @[@"outer", @"blockStack", @"external"]) {
id candidate = views[name];
UIView *view = candidate == NSNull.null ? nil : candidate;
rawHosts[name] = IDPrivateTopologyRawHostState(name, view, outer);
}
NSUInteger liveHostCount = 0;
NSMutableDictionary<NSString *, UIView *> *nullableViews = [views mutableCopy];
if (inner == nil) [nullableViews removeObjectForKey:@"blockStack"];
if (external == nil) [nullableViews removeObjectForKey:@"external"];
NSString *installationHost = IDPrivateTopologyInstallationHost(
rawHosts, nullableViews, outer, &liveHostCount);
NSDictionary<NSString *, id> *baseline = objc_getAssociatedObject(
outer, IDPrivateInteractionHostTopologyBaselineKey);
if (baseline == nil) {
NSMutableDictionary *baselineHosts = [NSMutableDictionary dictionary];
for (NSString *name in @[@"outer", @"blockStack", @"external"]) {
baselineHosts[name] = @{
@"recognizerIdentitySignature":
rawHosts[name][@"recognizerIdentitySignature"],
@"interactionIdentitySignature":
rawHosts[name][@"interactionIdentitySignature"],
};
}
baseline = @{
@"installationHost": installationHost,
@"hosts": baselineHosts.copy,
};
objc_setAssociatedObject(outer,
IDPrivateInteractionHostTopologyBaselineKey, baseline,
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
BOOL stable = [baseline[@"installationHost"] isEqualToString:installationHost];
NSMutableDictionary<NSString *, NSDictionary<NSString *, id> *> *hosts =
[NSMutableDictionary dictionary];
for (NSString *name in @[@"outer", @"blockStack", @"external"]) {
NSDictionary *raw = rawHosts[name];
NSDictionary *baselineHost = baseline[@"hosts"][name];
NSArray *recognizerIdentities = raw[@"recognizerIdentitySignature"];
NSArray *interactionIdentities = raw[@"interactionIdentitySignature"];
NSArray *baselineRecognizers =
baselineHost[@"recognizerIdentitySignature"] ?: @[];
NSArray *baselineInteractions =
baselineHost[@"interactionIdentitySignature"] ?: @[];
if (![recognizerIdentities isEqualToArray:baselineRecognizers] ||
![interactionIdentities isEqualToArray:baselineInteractions]) {
stable = NO;
}
NSMutableDictionary *host = [raw mutableCopy];
host[@"baselineRecognizerIdentitySignature"] = baselineRecognizers;
host[@"baselineInteractionIdentitySignature"] = baselineInteractions;
host[@"addedRecognizerIdentitySignature"] =
IDPrivateTopologySortedDifference(recognizerIdentities, baselineRecognizers);
host[@"addedInteractionIdentitySignature"] =
IDPrivateTopologySortedDifference(interactionIdentities, baselineInteractions);
host[@"removedRecognizerIdentitySignature"] =
IDPrivateTopologySortedDifference(baselineRecognizers, recognizerIdentities);
host[@"removedInteractionIdentitySignature"] =
IDPrivateTopologySortedDifference(baselineInteractions, interactionIdentities);
hosts[name] = host.copy;
}
return @{
@"schema": @1,
@"installationHost": installationHost,
@"liveHostCount": @(liveHostCount),
@"duplicateRecognizerIdentityCount":
@(IDPrivateTopologyDuplicateIdentityCount(
rawHosts, @"recognizerIdentitySignature")),
@"duplicateInteractionIdentityCount":
@(IDPrivateTopologyDuplicateIdentityCount(
rawHosts, @"interactionIdentitySignature")),
@"stableForEpoch": @(stable),
@"epochInstallationHost": baseline[@"installationHost"],
@"hosts": hosts.copy,
};
}
void IDPrivateResetInteractionHostTopologyBaselineForUITesting(
UIView *outer,
UIView *inner,
UIView *external) {
(void)inner;
(void)external;
objc_setAssociatedObject(outer,
IDPrivateInteractionHostTopologyBaselineKey, nil,
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
static __attribute__((unused)) void IDPrivateStockOracleSelectorInventory(void) {
(void)NSSelectorFromString(TGPN_interactionAssistant());
(void)NSSelectorFromString(TGPN_selectionContainerView());
(void)NSSelectorFromString(@"id_selectionPresentationBackend");
(void)NSSelectorFromString(@"existingSelectionContainerView");
(void)NSSelectorFromString(@"selectionContainerCreationCount");
(void)NSSelectorFromString(TGPN_activeInstance());
(void)NSSelectorFromString(TGPN_keyboardStateManager());
(void)NSSelectorFromString(TGPN_inputDelegateManager());
}
static IMP IDPrivateStockOriginalCursorPositionIMP;
static NSUInteger IDPrivateStockCursorSequence;
static CGPoint IDPrivateStockCursorPoint;
static __weak UIView *IDPrivateStockCursorEditor;
static NSInteger IDPrivateStockCursorOutputOffset;
static NSInteger IDPrivateStockCursorOutputAffinity;
static BOOL IDPrivateStockCursorOutputValid;
static BOOL IDPrivateStockCursorUsedFirstContainer;
static const void *IDPrivateFloatingTerminalObserverKey =
&IDPrivateFloatingTerminalObserverKey;
static const void *IDPrivateFloatingActionObserverKey =
&IDPrivateFloatingActionObserverKey;
static void IDPrivateStockStartAutoscroll(id self, SEL selector, CGPoint point) {
UIView *view = self;
IDPrivateRecordTextAutoscrollStartForUITesting(
self, point, point, view.bounds, view.bounds, 0, @"uikit");
struct objc_super superInfo = { self, class_getSuperclass(object_getClass(self)) };
((void (*)(struct objc_super *, SEL, CGPoint))objc_msgSendSuper)(
&superInfo, selector, point);
}
static void IDPrivateStockCancelAutoscroll(id self, SEL selector) {
IDPrivateRecordTextAutoscrollCancelForUITesting(self);
struct objc_super superInfo = { self, class_getSuperclass(object_getClass(self)) };
((void (*)(struct objc_super *, SEL))objc_msgSendSuper)(&superInfo, selector);
}
static void IDPrivateStockEndFloatingCursor(id self, SEL selector) {
IDPrivateRecordFloatingLifecycleEventForUITesting(self, @"normalEnd");
struct objc_super superInfo = { self, class_getSuperclass(object_getClass(self)) };
((void (*)(struct objc_super *, SEL))objc_msgSendSuper)(&superInfo, selector);
}
static BOOL IDPrivateStockResignFirstResponder(id self, SEL selector) {
BOOL wasFirstResponder = [self isFirstResponder];
struct objc_super superInfo = { self, class_getSuperclass(object_getClass(self)) };
BOOL result = ((BOOL (*)(struct objc_super *, SEL))objc_msgSendSuper)(&superInfo, selector);
if (result && wasFirstResponder) {
IDPrivateRecordFloatingLifecycleEventForUITesting(self, @"responderLoss");
}
return result;
}
static void IDPrivateStockSetEditable(id self, SEL selector, BOOL editable) {
BOOL changedOff = ((BOOL (*)(id, SEL))objc_msgSend)(
self, NSSelectorFromString(@"isEditable")) && !editable;
struct objc_super superInfo = { self, class_getSuperclass(object_getClass(self)) };
((void (*)(struct objc_super *, SEL, BOOL))objc_msgSendSuper)(
&superInfo, selector, editable);
if (changedOff) IDPrivateRecordFloatingLifecycleEventForUITesting(self, @"editableOff");
}
static void IDPrivateStockSetSelectable(id self, SEL selector, BOOL selectable) {
BOOL changedOff = ((BOOL (*)(id, SEL))objc_msgSend)(
self, NSSelectorFromString(@"isSelectable")) && !selectable;
struct objc_super superInfo = { self, class_getSuperclass(object_getClass(self)) };
((void (*)(struct objc_super *, SEL, BOOL))objc_msgSendSuper)(
&superInfo, selector, selectable);
if (changedOff) IDPrivateRecordFloatingLifecycleEventForUITesting(self, @"selectableOff");
}
static UITextPosition *IDPrivateStockTracedCursorPosition(
id controller, SEL selector, CGPoint point, id container) {
UITextPosition *(*original)(id, SEL, CGPoint, id) =
(UITextPosition *(*)(id, SEL, CGPoint, id))IDPrivateStockOriginalCursorPositionIMP;
UITextPosition *result = original(controller, selector, point, container);
id first = ((id (*)(id, SEL))objc_msgSend)(
controller, NSSelectorFromString(TGPN_firstTextContainer()));
@synchronized (controller) {
UIView *editor = IDPrivateStockCursorEditor;
id<UITextInput> input = (id<UITextInput>)editor;
NSInteger offset = editor == nil || result == nil ? NSNotFound :
[input offsetFromPosition:input.beginningOfDocument toPosition:result];
NSInteger affinity = result == nil ? UITextStorageDirectionForward :
((NSInteger (*)(id, SEL, id))objc_msgSend)(controller,
NSSelectorFromString(TGPN_affinityForPosition_()), result);
IDPrivateStockCursorSequence += 1;
IDPrivateStockCursorPoint = point;
IDPrivateStockCursorOutputOffset = offset;
IDPrivateStockCursorOutputAffinity = affinity;
IDPrivateStockCursorOutputValid = offset >= 0;
IDPrivateStockCursorUsedFirstContainer = container == first;
}
return result;
}
UIView *IDPrivateNewStockOracleTextView(Class baseClass, CGRect frame) {
static Class stockClass;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSCAssert(baseClass != Nil, @"Stock oracle base class is required");
stockClass = objc_allocateClassPair(baseClass, "IDPrivateStockOracleTextView", 0);
NSDictionary<NSString *, NSValue *> *methods = @{
TGPN_startAutoscroll_(): [NSValue valueWithPointer:IDPrivateStockStartAutoscroll],
@"cancelAutoscroll": [NSValue valueWithPointer:IDPrivateStockCancelAutoscroll],
@"endFloatingCursor": [NSValue valueWithPointer:IDPrivateStockEndFloatingCursor],
@"resignFirstResponder": [NSValue valueWithPointer:IDPrivateStockResignFirstResponder],
@"setEditable:": [NSValue valueWithPointer:IDPrivateStockSetEditable],
@"setSelectable:": [NSValue valueWithPointer:IDPrivateStockSetSelectable],
};
[methods enumerateKeysAndObjectsUsingBlock:^(NSString *name, NSValue *impValue,
BOOL *stop) {
(void)stop;
SEL selector = NSSelectorFromString(name);
Method method = class_getInstanceMethod(baseClass, selector);
NSCAssert(method != NULL, @"Stock oracle base is missing %@", name);
class_addMethod(stockClass, selector, impValue.pointerValue,
method_getTypeEncoding(method));
}];
objc_registerClassPair(stockClass);
});
id value = ((id (*)(id, SEL))objc_msgSend)(stockClass, sel_registerName("alloc"));
return ((id (*)(id, SEL, CGRect, id))objc_msgSend)(
value, NSSelectorFromString(@"initWithFrame:textContainer:"), frame, nil);
}
void IDPrivateInstallStockCursorTraceForUITesting(UIView *editor) {
id controller = IDPrivateTestingInternalTextLayoutController(editor);
if (controller == nil) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"Stock cursor trace layout controller is unavailable" userInfo:nil];
}
IDPrivateStockCursorEditor = editor;
IDPrivateStockCursorSequence = 0;
IDPrivateStockCursorOutputValid = NO;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL selector = NSSelectorFromString(TGPN_cursorPositionAtPoint_inContainer_());
Method method = class_getInstanceMethod(object_getClass(controller), selector);
if (method == NULL) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"Stock cursor trace selector is unavailable" userInfo:nil];
}
IDPrivateStockOriginalCursorPositionIMP = method_getImplementation(method);
method_setImplementation(method, (IMP)IDPrivateStockTracedCursorPosition);
});
}
NSDictionary<NSString *, id> *IDPrivateCopyStockCursorTraceForUITesting(
UIView *editor) {
id controller = IDPrivateTestingInternalTextLayoutController(editor);
@synchronized (controller) {
if (IDPrivateStockCursorSequence == 0 || !IDPrivateStockCursorOutputValid) return nil;
return @{
@"sequence": @(IDPrivateStockCursorSequence), @"kind": @"cursor",
@"interactionContainer": @(IDPrivateStockCursorUsedFirstContainer),
@"point": IDPrivatePointJSON(IDPrivateStockCursorPoint),
@"output": @{@"offset": @(IDPrivateStockCursorOutputOffset),
@"affinity": @(IDPrivateStockCursorOutputAffinity)},
};
}
}
static NSDictionary<NSString *, NSNumber *> *IDPrivatePointJSON(CGPoint point) {
return @{ @"x": @(point.x), @"y": @(point.y) };
}
static NSDictionary<NSString *, NSNumber *> *IDPrivateRectJSON(CGRect rect) {
return @{ @"x": @(rect.origin.x), @"y": @(rect.origin.y),
@"width": @(rect.size.width), @"height": @(rect.size.height) };
}
void IDPrivateRecordTextAutoscrollStartForUITesting(
UIScrollView *scrollContainer, CGPoint assistantPoint, CGPoint hostPoint,
CGRect visible, CGRect safe, NSUInteger directions, NSString *engine) {
NSDictionary *old = objc_getAssociatedObject(scrollContainer,
IDPrivateTextAutoscrollTraceKey);
NSMutableDictionary *trace = [old mutableCopy] ?: [NSMutableDictionary dictionary];
trace[@"startCount"] = @([old[@"startCount"] unsignedIntegerValue] + 1);
trace[@"cancelCount"] = old[@"cancelCount"] ?: @0;
trace[@"assistantPoint"] = IDPrivatePointJSON(assistantPoint);
trace[@"hostPoint"] = IDPrivatePointJSON(hostPoint);
trace[@"visible"] = IDPrivateRectJSON(visible);
trace[@"safe"] = IDPrivateRectJSON(safe);
trace[@"directions"] = @(directions);
trace[@"engine"] = [engine copy];
objc_setAssociatedObject(scrollContainer, IDPrivateTextAutoscrollTraceKey,
trace.copy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
void IDPrivateRecordTextAutoscrollCancelForUITesting(UIScrollView *scrollContainer) {
NSDictionary *old = objc_getAssociatedObject(scrollContainer,
IDPrivateTextAutoscrollTraceKey);
NSMutableDictionary *trace = [old mutableCopy] ?: [NSMutableDictionary dictionary];
trace[@"startCount"] = old[@"startCount"] ?: @0;
trace[@"cancelCount"] = @([old[@"cancelCount"] unsignedIntegerValue] + 1);
objc_setAssociatedObject(scrollContainer, IDPrivateTextAutoscrollTraceKey,
trace.copy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
NSDictionary<NSString *, id> *IDPrivateCopyTextAutoscrollTraceForUITesting(
UIScrollView *scrollContainer) {
return [objc_getAssociatedObject(scrollContainer,
IDPrivateTextAutoscrollTraceKey) copy] ?: @{};
}
void IDPrivateRecordFloatingLifecycleEventForUITesting(
UIScrollView *scrollContainer, NSString *event) {
NSMutableDictionary *trace = [IDPrivateCopyTextAutoscrollTraceForUITesting(
scrollContainer) mutableCopy];
NSString *key = [event stringByAppendingString:@"Count"];
trace[key] = @([trace[key] unsignedIntegerValue] + 1);
trace[@"editable"] = @([(id)scrollContainer isEditable]);
trace[@"selectable"] = @([(id)scrollContainer isSelectable]);
objc_setAssociatedObject(scrollContainer, IDPrivateTextAutoscrollTraceKey,
trace.copy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
NSDictionary *terminal = objc_getAssociatedObject(
scrollContainer, IDPrivateFloatingTerminalObserverKey);
if ([terminal[@"event"] isEqualToString:event]) {
IDPrivateFloatingLifecycleTerminalObserver observer = terminal[@"observer"];
NSUInteger count = [trace[key] unsignedIntegerValue];
CFTimeInterval terminalEpoch = CACurrentMediaTime();
objc_setAssociatedObject(scrollContainer, IDPrivateFloatingTerminalObserverKey,
nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if (observer != nil) {
NSTimer *timer = [NSTimer timerWithTimeInterval:0 repeats:NO
block:^(__unused NSTimer *scheduledTimer) {
observer(event, count, terminalEpoch);
}];
[[NSRunLoop mainRunLoop] addTimer:timer
forMode:NSRunLoopCommonModes];
}
}
NSDictionary *action = objc_getAssociatedObject(
scrollContainer, IDPrivateFloatingActionObserverKey);
if ([action[@"event"] isEqualToString:event]) {
IDPrivateFloatingLifecycleTerminalObserver observer = action[@"observer"];
NSUInteger count = [trace[key] unsignedIntegerValue];
CFTimeInterval actionEpoch = CACurrentMediaTime();
objc_setAssociatedObject(scrollContainer, IDPrivateFloatingActionObserverKey,
nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if (observer != nil) observer(event, count, actionEpoch);
}
}
void IDPrivateSetFloatingLifecycleTerminalObserverForUITesting(
UIScrollView *scrollContainer, NSString *event,
IDPrivateFloatingLifecycleTerminalObserver observer) {
if (observer == nil) {
objc_setAssociatedObject(scrollContainer, IDPrivateFloatingTerminalObserverKey,
nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return;
}
objc_setAssociatedObject(scrollContainer, IDPrivateFloatingTerminalObserverKey,
@{@"event": [event copy], @"observer": [observer copy]},
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
void IDPrivateSetFloatingLifecycleActionObserverForUITesting(
UIScrollView *scrollContainer, NSString *event,
IDPrivateFloatingLifecycleTerminalObserver observer) {
if (observer == nil) {
objc_setAssociatedObject(scrollContainer, IDPrivateFloatingActionObserverKey,
nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return;
}
objc_setAssociatedObject(scrollContainer, IDPrivateFloatingActionObserverKey,
@{@"event": [event copy], @"observer": [observer copy]},
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
static CGRect IDPrivateTextAutoscrollSafeRectWithGeometry(
CGRect visible, UIEdgeInsets textInsets, UIEdgeInsets adjustment,
CGFloat horizontalAdjustment, UIEdgeInsets effective, CGFloat edgeInset) {
textInsets.top = MAX(textInsets.top, adjustment.top);
textInsets.left = MAX(textInsets.left, adjustment.left);
textInsets.bottom = MAX(textInsets.bottom, adjustment.bottom);
textInsets.right = MAX(textInsets.right, adjustment.right);
if (horizontalAdjustment < 0) textInsets.left = horizontalAdjustment;
visible.origin.x += textInsets.left;
visible.origin.y += textInsets.top;
visible.size.width -= textInsets.left + textInsets.right;
visible.size.height -= textInsets.top + textInsets.bottom;
visible.origin.x += effective.left;
visible.origin.y += effective.top;
visible.size.width -= effective.left + effective.right;
visible.size.height -= effective.top + effective.bottom;
return CGRectInset(visible, edgeInset, edgeInset);
}
CGRect IDPrivateTestingTextAutoscrollSafeRectWithGeometry(
CGRect visible, UIEdgeInsets textInsets, UIEdgeInsets insetAdjustment,
CGFloat horizontalAdjustment, UIEdgeInsets effectiveContentInset,
CGFloat edgeInset) {
return IDPrivateTextAutoscrollSafeRectWithGeometry(
visible, textInsets, insetAdjustment, horizontalAdjustment,
effectiveContentInset, edgeInset);
}
UIScrollView *IDPrivateTestingNewTextAutoscrollHostWithIncompatibleIvar(
IDPrivateTestingIncompatibleAutoscrollIvar incompatibleIvar) {
NSString *className = [NSString stringWithFormat:
@"IDPrivateWrongAutoscrollIvarHost%lu", (unsigned long)incompatibleIvar];
Class hostClass = NSClassFromString(className);
if (hostClass == Nil) {
hostClass = objc_allocateClassPair(UIScrollView.class, className.UTF8String, 0);
const char *ivarName = incompatibleIvar ==
IDPrivateTestingIncompatibleAutoscrollIvarTextContainerInsetAdjustment
? TGPN__textContainerInsetAdjustment().UTF8String
: TGPN__horizontalTextContainerGeometryAdjustment().UTF8String;
BOOL added = class_addIvar(hostClass, ivarName, sizeof(uint8_t), 0,
@encode(uint8_t));
if (!added) {
objc_disposeClassPair(hostClass);
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"Could not construct incompatible autoscroll test host"
userInfo:nil];
}
objc_registerClassPair(hostClass);
}
UIScrollView *host = [[hostClass alloc] initWithFrame:CGRectMake(0, 0, 320, 200)];
host.contentSize = CGSizeMake(320, 1000);
return host;
}
static void IDPrivateRequireSignature(id target, SEL selector,
const char *returnType,
NSUInteger argumentCount,
const char * _Nullable argumentType) {
NSMethodSignature *signature = [target methodSignatureForSelector:selector];
BOOL valid = signature != nil && signature.numberOfArguments == argumentCount &&
strcmp(signature.methodReturnType, returnType) == 0;
if (valid && argumentType != NULL) {
valid = strcmp([signature getArgumentTypeAtIndex:2], argumentType) == 0;
}
if (!valid) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:[NSString stringWithFormat:@"Private operation ABI is unavailable for %@",
NSStringFromSelector(selector)]
userInfo:nil];
}
}
static const char *IDPrivateSkipEncodingFieldName(const char *encoding) {
if (*encoding != '"') return encoding;
encoding += 1;
while (*encoding != '\0' && *encoding != '"') {
if (*encoding == '\\' && encoding[1] != '\0') encoding += 1;
encoding += 1;
}
return *encoding == '"' ? encoding + 1 : encoding;
}
static BOOL IDPrivateTypeEncodingsMatch(const char *actual, const char *expected) {
while (*actual != '\0' || *expected != '\0') {
actual = IDPrivateSkipEncodingFieldName(actual);
expected = IDPrivateSkipEncodingFieldName(expected);
if (*actual != *expected) return NO;
if (*actual != '\0') actual += 1;
if (*expected != '\0') expected += 1;
}
return YES;
}
static void IDPrivateRequireIvarABI(Ivar ivar, const char *expectedEncoding,
NSString *name) {
const char *encoding = ivar_getTypeEncoding(ivar);
NSUInteger actualSize = 0;
NSUInteger actualAlignment = 0;
NSUInteger expectedSize = 0;
NSUInteger expectedAlignment = 0;
if (encoding != NULL) {
NSGetSizeAndAlignment(encoding, &actualSize, &actualAlignment);
}
NSGetSizeAndAlignment(expectedEncoding, &expectedSize, &expectedAlignment);
if (encoding == NULL || !IDPrivateTypeEncodingsMatch(encoding, expectedEncoding) ||
actualSize != expectedSize || actualAlignment != expectedAlignment) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:[NSString stringWithFormat:@"Recovered %@ ABI is incompatible", name]
userInfo:nil];
}
}
UITextPosition *IDPrivateTestingCursorPositionAtPoint(
UIView *textInput, id layoutController, CGPoint point, id *textContainer) {
if (layoutController == nil) {
SEL controllerSelector = NSSelectorFromString(TGPN__internalTextLayoutController());
IDPrivateRequireSignature(textInput, controllerSelector, @encode(id), 2, NULL);
layoutController = ((id (*)(id, SEL))objc_msgSend)(textInput, controllerSelector);
}
SEL containerSelector = NSSelectorFromString(TGPN_firstTextContainer());
IDPrivateRequireSignature(layoutController, containerSelector, @encode(id), 2, NULL);
id container = ((id (*)(id, SEL))objc_msgSend)(layoutController, containerSelector);
SEL cursorSelector = NSSelectorFromString(TGPN_cursorPositionAtPoint_inContainer_());
NSMethodSignature *signature = [layoutController methodSignatureForSelector:cursorSelector];
BOOL valid = signature != nil && signature.numberOfArguments == 4 &&
strcmp(signature.methodReturnType, @encode(id)) == 0 &&
strcmp([signature getArgumentTypeAtIndex:2], @encode(CGPoint)) == 0 &&
strcmp([signature getArgumentTypeAtIndex:3], @encode(id)) == 0;
if (!valid) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"Private cursor resolution ABI is unavailable" userInfo:nil];
}
if (textContainer != NULL) *textContainer = container;
return ((id (*)(id, SEL, CGPoint, id))objc_msgSend)(
layoutController, cursorSelector, point, container);
}
id IDPrivateTestingInternalTextLayoutController(UIView *textInput) {
SEL selector = NSSelectorFromString(TGPN__internalTextLayoutController());
IDPrivateRequireSignature(textInput, selector, @encode(id), 2, NULL);
return ((id (*)(id, SEL))objc_msgSend)(textInput, selector);
}
id IDPrivateTestingCustomRenderController(UIView *textInput) {
SEL selector = NSSelectorFromString(TGPN_customRenderController());
IDPrivateRequireSignature(textInput, selector, @encode(id), 2, NULL);
return ((id (*)(id, SEL))objc_msgSend)(textInput, selector);
}
CGPoint IDPrivateTestingConvertPointFromRenderSpace(UIView *textInput, CGPoint point) {
SEL selector = NSSelectorFromString(TGPN__convertPointFromRenderSpace_());
IDPrivateRequireSignature(textInput, selector, @encode(CGPoint), 3, @encode(CGPoint));
return ((CGPoint (*)(id, SEL, CGPoint))objc_msgSend)(textInput, selector, point);
}
CGPoint IDPrivateTestingTextContainerOrigin(UIView *containerView) {
SEL selector = NSSelectorFromString(TGPN_textContainerOrigin());
IDPrivateRequireSignature(containerView, selector, @encode(CGPoint), 2, NULL);
return ((CGPoint (*)(id, SEL))objc_msgSend)(containerView, selector);
}
void IDPrivateTestingWithTextContainerInsetAdjustment(
UIScrollView *scrollContainer, UIEdgeInsets adjustment,
void (NS_NOESCAPE ^body)(void)) {
Ivar ivar = class_getInstanceVariable(scrollContainer.class,
TGPN__textContainerInsetAdjustment().UTF8String);
if (ivar == NULL) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"Private inset-adjustment testing ABI is unavailable" userInfo:nil];
}
IDPrivateRequireIvarABI(ivar, @encode(UIEdgeInsets),
@"testing text-container inset adjustment");
uint8_t *bytes = (uint8_t *)(__bridge void *)scrollContainer + ivar_getOffset(ivar);
UIEdgeInsets original = UIEdgeInsetsZero;
memcpy(&original, bytes, sizeof(original));
memcpy(bytes, &adjustment, sizeof(adjustment));
@try {
body();
} @finally {
memcpy(bytes, &original, sizeof(original));
}
}
CGRect IDPrivateTextAutoscrollSafeRect(UIScrollView *scrollContainer,
UIEdgeInsets textInsets,
CGFloat edgeInset) {
SEL visibleSelector = NSSelectorFromString(@"visibleBounds");
SEL effectiveInsetSelector = NSSelectorFromString(TGPN__effectiveContentInset());
NSMethodSignature *visibleSignature =
[scrollContainer methodSignatureForSelector:visibleSelector];
NSMethodSignature *effectiveSignature =
[scrollContainer methodSignatureForSelector:effectiveInsetSelector];
if (visibleSignature == nil || visibleSignature.numberOfArguments != 2 ||
strcmp(visibleSignature.methodReturnType, @encode(CGRect)) != 0 ||
effectiveSignature == nil || effectiveSignature.numberOfArguments != 2 ||
strcmp(effectiveSignature.methodReturnType, @encode(UIEdgeInsets)) != 0) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"Recovered text autoscroll geometry ABI is unavailable"
userInfo:nil];
}
CGRect visible = ((CGRect (*)(id, SEL))objc_msgSend)(scrollContainer,
visibleSelector);
UIEdgeInsets adjustment = UIEdgeInsetsZero;
Ivar adjustmentIvar = class_getInstanceVariable(scrollContainer.class,
TGPN__textContainerInsetAdjustment().UTF8String);
if (adjustmentIvar != NULL) {
IDPrivateRequireIvarABI(adjustmentIvar, @encode(UIEdgeInsets),
@"text-container inset adjustment");
memcpy(&adjustment,
(uint8_t *)(__bridge void *)scrollContainer + ivar_getOffset(adjustmentIvar),
sizeof(adjustment));
}
CGFloat horizontalAdjustment = 0;
Ivar horizontalIvar = class_getInstanceVariable(scrollContainer.class,
TGPN__horizontalTextContainerGeometryAdjustment().UTF8String);
if (horizontalIvar != NULL) {
IDPrivateRequireIvarABI(horizontalIvar, @encode(CGFloat),
@"horizontal text-container geometry adjustment");
memcpy(&horizontalAdjustment,
(uint8_t *)(__bridge void *)scrollContainer + ivar_getOffset(horizontalIvar),
sizeof(horizontalAdjustment));
}
UIEdgeInsets effective = ((UIEdgeInsets (*)(id, SEL))objc_msgSend)(
scrollContainer, effectiveInsetSelector);
return IDPrivateTextAutoscrollSafeRectWithGeometry(
visible, textInsets, adjustment, horizontalAdjustment, effective, edgeInset);
}
NSUInteger IDPrivateTextAutoscrollDirections(UIScrollView *scrollContainer,
UIEdgeInsets textInsets,
CGPoint point,
CGFloat edgeInset) {
CGRect safe = IDPrivateTextAutoscrollSafeRect(scrollContainer, textInsets, edgeInset);
if (CGRectGetHeight(safe) + 2.0 * edgeInset >= scrollContainer.contentSize.height) return 0;
if (point.y > CGRectGetMaxY(safe)) return 8U;
if (point.y < CGRectGetMinY(safe)) return 4U;
return 0;
}
static CGRect IDPrivateVisibleDescendantBoundsInView(UIView *view, UIView *coordinateView) {
if (view.hidden || view.alpha <= 0.01) return CGRectNull;
CGRect result = [view convertRect:view.bounds toView:coordinateView];
for (UIView *subview in view.subviews) {
CGRect child = IDPrivateVisibleDescendantBoundsInView(subview, coordinateView);
if (!CGRectIsNull(child)) result = CGRectUnion(result, child);
}
return result;
}
NSArray<NSValue *> *IDPrivateVisibleSelectionHandleRects(UIView *root) {
NSMutableArray<NSValue *> *result = [NSMutableArray array];
NSMutableArray<UIView *> *pending = [NSMutableArray arrayWithObject:root];
while (pending.count > 0) {
UIView *view = pending.lastObject;
[pending removeLastObject];
[pending addObjectsFromArray:view.subviews];
if (![NSStringFromClass(view.class) containsString:TGPN__UITextSelectionLollipopView()] ||
view.hidden || view.alpha <= 0.01) continue;
CGRect rect = IDPrivateVisibleDescendantBoundsInView(view, root);
if (!CGRectIsNull(rect) && !CGRectIsEmpty(rect))
[result addObject:[NSValue valueWithCGRect:rect]];
}
return result.copy;
}
typedef struct {
__unsafe_unretained NSString *className;
BOOL classMethod;
SEL selector;
BOOL required;
} IDContract;
// telegram-ios DEVIATION 2 (the only source edit in the 78-file port; see PROVENANCE.md).
// The vendored kernel carve Host/TGRichTextInputDecHostPreflight.m ALSO defines a class
// named IDInteractionABISpec -- it was carved out of THIS file. Both link into the same
// binary, so the two would collide as duplicate _OBJC_CLASS_$_IDInteractionABISpec symbols.
// The carve is vendor-owned and must not be edited, so the rename happens here, and as a
// runtime-name attribute rather than an identifier rename so that the other 47 references
// to this class stay byte-identical to upstream.
__attribute__((objc_runtime_name("IDUpstreamInteractionABISpec")))
@interface IDInteractionABISpec : NSObject
@property(nonatomic, copy, readonly) NSString *selectorName;
@property(nonatomic, readonly) const char *returnType;
@property(nonatomic, copy, readonly) NSArray<NSString *> *argumentTypes;
- (instancetype)initWithSelectorName:(NSString *)selectorName
returnType:(const char *)returnType
argumentTypes:(NSArray<NSString *> *)argumentTypes;
@end
@implementation IDInteractionABISpec
- (instancetype)initWithSelectorName:(NSString *)selectorName
returnType:(const char *)returnType
argumentTypes:(NSArray<NSString *> *)argumentTypes {
self = [super init];
if (self) {
_selectorName = [selectorName copy];
_returnType = returnType;
_argumentTypes = [argumentTypes copy];
}
return self;
}
@end
static IDInteractionABISpec *IDInteractionABISpecMake(
NSString *selectorName,
const char *returnType,
NSArray<NSString *> *argumentTypes) {
return [[IDInteractionABISpec alloc] initWithSelectorName:selectorName
returnType:returnType
argumentTypes:argumentTypes];
}
static NSString *IDInteractionTypeEncoding(const char *encoding) {
return encoding == NULL ? @"<missing>" : [NSString stringWithUTF8String:encoding];
}
static NSArray<IDInteractionABISpec *> *IDInteractionHostABISpecs(void) {
return @[
IDInteractionABISpecMake(TGPN_interactionAssistant(), @encode(id), @[]),
IDInteractionABISpecMake(TGPN__textInputViewForAddingGestureRecognizers(), @encode(id), @[]),
IDInteractionABISpecMake(TGPN_selectionContainerView(), @encode(id), @[]),
];
}
static const char *IDInteractionSkipTypeQualifiers(const char *encoding) {
while (encoding != NULL && strchr("rnNoORV", encoding[0]) != NULL) {
encoding += 1;
}
return encoding;
}
static BOOL IDInteractionTypeMatches(const char *actual, const char *expected) {
actual = IDInteractionSkipTypeQualifiers(actual);
expected = IDInteractionSkipTypeQualifiers(expected);
if (actual == NULL || expected == NULL) return actual == expected;
if (expected[0] == '@' && expected[1] == '\0') {
return actual[0] == '@' && actual[1] != '?';
}
return strcmp(actual, expected) == 0;
}
static NSString *IDInteractionABIMismatchForSignature(
NSMethodSignature *signature,
IDInteractionABISpec *spec) {
if (signature == nil) {
return [NSString stringWithFormat:@"%@ is missing", spec.selectorName];
}
NSUInteger explicitCount = signature.numberOfArguments >= 2
? signature.numberOfArguments - 2
: 0;
BOOL matches = IDInteractionTypeMatches(signature.methodReturnType, spec.returnType) &&
explicitCount == spec.argumentTypes.count;
if (matches) {
for (NSUInteger index = 0; index < explicitCount; index += 1) {
if (!IDInteractionTypeMatches([signature getArgumentTypeAtIndex:index + 2],
spec.argumentTypes[index].UTF8String)) {
matches = NO;
break;
}
}
}
if (matches) return nil;
NSMutableArray<NSString *> *actualArguments = [NSMutableArray array];
for (NSUInteger index = 0; index < explicitCount; index += 1) {
[actualArguments addObject:IDInteractionTypeEncoding(
[signature getArgumentTypeAtIndex:index + 2])];
}
return [NSString stringWithFormat:
@"%@ expected return %s arguments (%@), got return %@ arguments (%@)",
spec.selectorName,
spec.returnType,
[spec.argumentTypes componentsJoinedByString:@", "],
IDInteractionTypeEncoding(signature.methodReturnType),
[actualArguments componentsJoinedByString:@", "]];
}
static NSString *IDInteractionABIMismatch(id target, IDInteractionABISpec *spec) {
SEL selector = NSSelectorFromString(spec.selectorName);
return IDInteractionABIMismatchForSignature(
[target methodSignatureForSelector:selector], spec);
}
static NSArray<NSString *> *IDInteractionInstanceABIMismatches(
Class cls,
NSArray<IDInteractionABISpec *> *specs) {
NSMutableArray<NSString *> *mismatches = [NSMutableArray array];
// Run `+initialize` first: it adds the callbacks UIKit calls under private names, and
// `class_getInstanceMethod` does not run it (`TGPrivateMethodAliases.h`).
(void)[cls class];
for (IDInteractionABISpec *spec in specs) {
Method method = class_getInstanceMethod(cls, NSSelectorFromString(spec.selectorName));
NSMethodSignature *signature = method == NULL ? nil :
[NSMethodSignature signatureWithObjCTypes:method_getTypeEncoding(method)];
NSString *mismatch = IDInteractionABIMismatchForSignature(signature, spec);
if (mismatch != nil) [mismatches addObject:mismatch];
}
return mismatches.copy;
}
static NSArray<NSString *> *IDInteractionABIMismatches(
id target,
NSArray<IDInteractionABISpec *> *specs) {
NSMutableArray<NSString *> *mismatches = [NSMutableArray array];
for (IDInteractionABISpec *spec in specs) {
NSString *mismatch = IDInteractionABIMismatch(target, spec);
if (mismatch != nil) [mismatches addObject:mismatch];
}
return mismatches.copy;
}
static NSDictionary<NSString *, id> *IDProductionContractDictionary(void) {
static NSDictionary<NSString *, id> *dictionary;
static dispatch_once_t once;
dispatch_once(&once, ^{
NSString *name = @"UIKitBehaviorContracts-iOS26.5";
NSURL *URL = [[NSBundle bundleForClass:IDPrivateUIKit.class]
URLForResource:name withExtension:@"json"];
if (URL == nil) {
URL = [NSBundle.mainBundle URLForResource:name
withExtension:@"json"];
}
NSData *data = URL == nil ? nil : [NSData dataWithContentsOfURL:URL];
// telegram-ios DEVIATION 5 (see PROVENANCE.md), site 2 of 2 — and this is the one that
// makes upstream's runtime gate REAL. Without it `data` is always nil here, this function
// returns @{}, IDRequiredContracts() below is EMPTY, and +missingContracts iterates zero
// rows — so requireCompatibleRuntime passes because it has nothing to check, on all ten
// capabilities, before every host this module stands up. Same one-line additive shape as
// site 1: upstream's lookup runs first, upstream's parse runs below, unchanged.
if (data == nil) { data = TGRichTextInputDecUpstreamPristineContractJSON(); }
id object = data == nil
? nil
: [NSJSONSerialization JSONObjectWithData:data
options:0
error:nil];
dictionary = [object isKindOfClass:NSDictionary.class]
? object
: @{};
});
return dictionary;
}
static NSArray<NSValue *> *IDRequiredContracts(void) {
static NSArray<NSValue *> *values;
static dispatch_once_t once;
dispatch_once(&once, ^{
NSDictionary<NSString *, id> *root =
IDProductionContractDictionary();
NSMutableArray<NSDictionary<NSString *, id> *> *members =
[NSMutableArray array];
for (NSString *sectionName in @[@"capabilities", @"referenceOnly"]) {
NSDictionary<NSString *, id> *section = root[sectionName];
NSArray<NSString *> *capabilityNames =
[section.allKeys sortedArrayUsingSelector:@selector(compare:)];
for (NSString *capabilityName in capabilityNames) {
NSArray<NSDictionary<NSString *, id> *> *capabilityMembers =
section[capabilityName][@"members"];
[members addObjectsFromArray:capabilityMembers ?: @[]];
}
}
NSMutableArray<NSValue *> *items =
[NSMutableArray arrayWithCapacity:members.count];
for (NSDictionary<NSString *, id> *member in members) {
NSString *kind = member[@"kind"];
if ([kind isEqualToString:@"protocol"]) {
continue;
}
IDContract contract = {
member[@"class"],
[kind isEqualToString:@"classMethod"],
NSSelectorFromString(member[@"name"]),
[member[@"required"] boolValue],
};
[items addObject:
[NSValue valueWithBytes:&contract
objCType:@encode(IDContract)]];
}
values = items.copy;
});
return values;
}
static NSArray<NSString *> *IDRequiredProtocolNames(void) {
static NSArray<NSString *> *names;
static dispatch_once_t once;
dispatch_once(&once, ^{
NSDictionary<NSString *, id> *root =
IDProductionContractDictionary();
NSMutableOrderedSet<NSString *> *required =
[NSMutableOrderedSet orderedSet];
for (NSString *sectionName in @[@"capabilities", @"referenceOnly"]) {
NSDictionary<NSString *, id> *section = root[sectionName];
for (NSDictionary<NSString *, id> *capability
in section.allValues) {
for (NSDictionary<NSString *, id> *member
in capability[@"members"] ?: @[]) {
if ([member[@"kind"] isEqualToString:@"protocol"] &&
[member[@"required"] boolValue]) {
[required addObject:member[@"name"]];
}
}
}
}
names = required.array;
});
return names;
}
@implementation IDPrivateUIKit
+ (void)requireCompatibleRuntime {
if (self.isCompatible) {
return;
}
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:self.compatibilityReport
userInfo:nil];
}
+ (void)requireObject:(id)object stage:(NSString *)stage {
if (object != nil) {
return;
}
NSString *reason = [NSString stringWithFormat:@"%@ failed.\n%@",
stage,
self.compatibilityReport];
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:reason
userInfo:nil];
}
+ (void)requireObject:(id)object
respondsToSelector:(SEL)selector
stage:(NSString *)stage {
if (object != nil && [object respondsToSelector:selector]) {
return;
}
NSString *reason = [NSString stringWithFormat:
@"%@ failed: result %@ does not respond to %@.\n%@",
stage,
object ?: @"<nil>",
NSStringFromSelector(selector),
self.compatibilityReport];
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:reason
userInfo:nil];
}
+ (NSArray<NSString *> *)missingContracts {
NSMutableArray *missing = [NSMutableArray array];
if (NSProtocolFromString(TGPN_UITextPasteConfigurationSupporting_Internal()) == nil) {
[missing addObject:[@"protocol " stringByAppendingString:TGPN_UITextPasteConfigurationSupporting_Internal()]];
}
for (NSString *protocolName in IDRequiredProtocolNames()) {
if (NSProtocolFromString(protocolName) == nil) {
[missing addObject:[@"protocol "
stringByAppendingString:protocolName]];
}
}
for (NSValue *value in IDRequiredContracts()) {
IDContract contract;
[value getValue:&contract];
Class cls = NSClassFromString(contract.className);
(void)[cls class]; // runs `+initialize`, which adds callbacks under private names
BOOL present = cls && (contract.classMethod
? [cls respondsToSelector:contract.selector]
: class_getInstanceMethod(cls, contract.selector) != NULL);
if (contract.required && !present) {
[missing addObject:[NSString stringWithFormat:@"%@ %@%@",
contract.className,
contract.classMethod ? @"+" : @"-",
NSStringFromSelector(contract.selector)]];
}
}
return missing;
}
+ (BOOL)isCompatible {
return self.missingContracts.count == 0;
}
+ (NSString *)compatibilityReport {
NSArray *missing = self.missingContracts;
__block NSUInteger requiredContractCount = 0;
for (NSValue *value in IDRequiredContracts()) {
IDContract contract;
[value getValue:&contract];
if (contract.required) requiredContractCount += 1;
}
Class baseClass = NSClassFromString(TGPN__UITextLayoutControllerBase());
SEL factory = NSSelectorFromString(TGPN_layoutControllerWithTextView_textContainer_());
NSString *layoutPath = [baseClass respondsToSelector:factory]
? [TGPN__UITextLayoutControllerBase() stringByAppendingString:@" factory"]
: [NSString stringWithFormat:@"direct %@ initializer (factory unavailable)", TGPN__UITextKit2LayoutController()];
// Assembled from parts: the private names are decoded at run time (`TGPrivateRuntimeNames.h`),
// so the report reads as it always did and no private name is compiled in as text.
NSArray<NSString *> *lines = @[
[@"iOS " stringByAppendingString:UIDevice.currentDevice.systemVersion],
[NSString stringWithFormat:@"%@ / %@", TGPN__UITextLayoutControllerBase(), TGPN__UITextKit2LayoutController()],
TGPN_UITextInputController(),
TGPN_UITextInteractionAssistant(),
TGPN_UITextPasteConfigurationSupporting_Internal(),
[@"Layout construction: " stringByAppendingString:layoutPath],
[NSString stringWithFormat:@"Canvas map: %@.%@ (by name, not by offset)",
TGPN__UITextKit2LayoutController(), TGPN__textCanvasViews()],
@"Viewport contract: NSTextViewportLayoutController layoutViewport",
@"Native fragment drawing path: NSTextLayoutFragment drawAtPoint:inContext:",
[NSString stringWithFormat:@"Continuous-spelling bridge: %@ %@",
TGPN_UITextInputController(), TGPN_setContinuousSpellCheckingEnabled_()],
@"Text-input traits bridge: controller-owned UITextInputTraits with full-family forwarding",
[NSString stringWithFormat:@"Required selector contracts preflighted: %lu", (unsigned long)requiredContractCount],
[NSString stringWithFormat:@"Optional/fallback: base layout factory, insertDictationResult:, %@, custom renderer presence",
TGPN_keyboardInputChangedSelection_()],
[NSString stringWithFormat:@"Construction result preflight: %@ -> %@",
TGPN__rangesForBackwardsDelete(), TGPN_unionRange()],
[NSString stringWithFormat:@"Action-time contract validation: %@, %@, custom renderer setNeedsDisplay",
TGPN_unionRange(), TGPN_textKit2Ranges()],
missing.count
? [@"Missing:\n" stringByAppendingString:[missing componentsJoinedByString:@"\n"]]
: @"All required contracts present",
];
return [lines componentsJoinedByString:@"\n"];
}
+ (id)newLayoutControllerWithTextView:(UIView *)view textContainer:(NSTextContainer *)container {
Class baseClass = NSClassFromString(TGPN__UITextLayoutControllerBase());
SEL factory = NSSelectorFromString(TGPN_layoutControllerWithTextView_textContainer_());
if ([baseClass respondsToSelector:factory]) {
return ((id (*)(id, SEL, id, id))objc_msgSend)(baseClass, factory, view, container);
}
Class concreteClass = NSClassFromString(TGPN__UITextKit2LayoutController());
return ((id (*)(id, SEL, id, id))objc_msgSend)(
[concreteClass alloc],
NSSelectorFromString(TGPN_initWithTextView_textContainer_()),
view,
container);
}
+ (id)newInputControllerWithLayoutController:(id)layoutController delegate:(id)delegate {
Class cls = NSClassFromString(TGPN_UITextInputController());
id controller = ((id (*)(id, SEL, id))objc_msgSend)(
[cls alloc],
NSSelectorFromString(TGPN_initWithTextLayoutController_()),
layoutController);
((void (*)(id, SEL, id))objc_msgSend)(
controller,
NSSelectorFromString(@"setDelegate:"),
delegate);
return controller;
}
+ (id)newInteractionAssistantWithView:(UIView *)view {
return [self newInteractionAssistantWithView:view
assistantClass:NSClassFromString(TGPN_UITextInteractionAssistant())];
}
+ (id)newInteractionAssistantWithView:(UIView *)view assistantClass:(Class)assistantClass {
__strong id assistant = [assistantClass alloc];
@try {
IDInteractionABISpec *initializer = IDInteractionABISpecMake(
@"initWithView:",
@encode(id),
@[IDInteractionTypeEncoding(@encode(id))]);
NSString *initializerMismatch = IDInteractionABIMismatch(assistant, initializer);
if (initializerMismatch != nil) {
NSString *reason = [NSString stringWithFormat:
@"Interaction assistant factory preflight failed: %@\n%@",
initializerMismatch,
self.compatibilityReport];
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:reason
userInfo:nil];
}
assistant = ((id (*)(id, SEL, id))objc_msgSend)(
assistant,
NSSelectorFromString(@"initWithView:"),
view);
[self requireInteractionAssistant:assistant
stage:@"Interaction assistant construction preflight"];
return assistant;
} @catch (NSException *exception) {
IDInteractionABISpec *clearRecognizers = IDInteractionABISpecMake(
TGPN_clearGestureRecognizers_(),
@encode(void),
@[IDInteractionTypeEncoding(@encode(BOOL))]);
NSException *cleanupException = nil;
if (assistant != nil &&
IDInteractionABIMismatch(assistant, clearRecognizers) == nil) {
@try {
((void (*)(id, SEL, BOOL))objc_msgSend)(
assistant,
NSSelectorFromString(TGPN_clearGestureRecognizers_()),
YES);
} @catch (NSException *caughtCleanupException) {
cleanupException = caughtCleanupException;
}
}
NSString *reason = [NSString stringWithFormat:
@"Interaction assistant initialization/adoption failed (%@): %@%@",
exception.name,
exception.reason ?: @"<no reason>",
cleanupException == nil
? @""
: [NSString stringWithFormat:@"; cleanup also failed (%@): %@",
cleanupException.name,
cleanupException.reason ?: @"<no reason>"]];
NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:exception
forKey:@"IDPrivateUIKitUnderlyingException"];
if (cleanupException != nil) {
userInfo[@"IDPrivateUIKitCleanupException"] = cleanupException;
}
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:reason
userInfo:userInfo.copy];
}
}
+ (void)requireInteractionHost:(id)host stage:(NSString *)stage {
NSArray<IDInteractionABISpec *> *specs = IDInteractionHostABISpecs();
NSArray<NSString *> *mismatches = IDInteractionABIMismatches(host, specs);
if (mismatches.count == 0) return;
NSString *reason = [NSString stringWithFormat:
@"%@ failed: interaction host ABI mismatch:\n%@",
stage,
[mismatches componentsJoinedByString:@"\n"]];
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:reason
userInfo:nil];
}
+ (void)requireInteractionAssistant:(id)assistant stage:(NSString *)stage {
NSArray<IDInteractionABISpec *> *specs = @[
IDInteractionABISpecMake(TGPN_selectionChanged(), @encode(void), @[]),
IDInteractionABISpecMake(TGPN_activateSelection(), @encode(void), @[]),
IDInteractionABISpecMake(TGPN_deactivateSelection(), @encode(void), @[]),
IDInteractionABISpecMake(TGPN_setSelectionDisplayVisible_(), @encode(void),
@[IDInteractionTypeEncoding(@encode(BOOL))]),
IDInteractionABISpecMake(TGPN_setNeedsSelectionDisplayUpdate(), @encode(void), @[]),
IDInteractionABISpecMake(TGPN_setGestureRecognizers(), @encode(void), @[]),
IDInteractionABISpecMake(TGPN_clearGestureRecognizers_(), @encode(void),
@[IDInteractionTypeEncoding(@encode(BOOL))]),
IDInteractionABISpecMake(@"beginFloatingCursorAtPoint:", @encode(void),
@[IDInteractionTypeEncoding(@encode(CGPoint))]),
IDInteractionABISpecMake(@"updateFloatingCursorAtPoint:animated:", @encode(void), @[
IDInteractionTypeEncoding(@encode(CGPoint)),
IDInteractionTypeEncoding(@encode(BOOL)),
]),
IDInteractionABISpecMake(@"endFloatingCursor", @encode(void), @[]),
IDInteractionABISpecMake(TGPN_resignedFirstResponder(), @encode(void), @[]),
IDInteractionABISpecMake(@"detach:", @encode(void), @[
IDInteractionTypeEncoding(@encode(BOOL)),
]),
];
NSArray<NSString *> *mismatches = assistant == nil
? @[@"assistant is nil"]
: IDInteractionABIMismatches(assistant, specs);
if (mismatches.count == 0) return;
NSString *reason = [NSString stringWithFormat:
@"%@ failed: interaction assistant ABI mismatch:\n%@\n%@",
stage,
[mismatches componentsJoinedByString:@"\n"],
self.compatibilityReport];
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:reason
userInfo:nil];
}
+ (void)installKeyboardInputSupportOnClass:(Class)viewClass {
Protocol *protocol = NSProtocolFromString(TGPN_UIKeyboardInput());
if (protocol == nil) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:[TGPN_UIKeyboardInput() stringByAppendingString:@" protocol is unavailable"]
userInfo:nil];
}
if (!class_conformsToProtocol(viewClass, protocol) &&
!class_addProtocol(viewClass, protocol)) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:[NSString stringWithFormat:@"Could not install iOS 26 %@ support", TGPN_UIKeyboardInput()]
userInfo:nil];
}
}
+ (void)installTextPasteSupportOnClass:(Class)viewClass {
Protocol *protocol = NSProtocolFromString(TGPN_UITextPasteConfigurationSupporting_Internal());
if (class_conformsToProtocol(viewClass, protocol)) {
return;
}
if (!class_addProtocol(viewClass, protocol)) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"Could not install iOS 26 internal text-paste support"
userInfo:nil];
}
}
+ (void)installTextAutoscrollingSupportOnClass:(Class)viewClass {
Protocol *protocol = NSProtocolFromString(TGPN_UITextAutoscrolling());
if (protocol == nil) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:[TGPN_UITextAutoscrolling() stringByAppendingString:@" protocol is unavailable"]
userInfo:nil];
}
NSArray<IDInteractionABISpec *> *specs = @[
IDInteractionABISpecMake(TGPN_startAutoscroll_(), @encode(void),
@[IDInteractionTypeEncoding(@encode(CGPoint))]),
IDInteractionABISpecMake(@"cancelAutoscroll", @encode(void), @[]),
];
NSArray<NSString *> *mismatches = IDInteractionInstanceABIMismatches(viewClass, specs);
if (mismatches.count != 0) {
NSString *reason = [NSString stringWithFormat:
@"%@ host ABI mismatch:\n%@", TGPN_UITextAutoscrolling(),
[mismatches componentsJoinedByString:@"\n"]];
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:reason
userInfo:nil];
}
if (!class_conformsToProtocol(viewClass, protocol) &&
!class_addProtocol(viewClass, protocol)) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:[NSString stringWithFormat:@"Could not install iOS 26 %@ support", TGPN_UITextAutoscrolling()]
userInfo:nil];
}
}
+ (BOOL)adoptResolvedTextAutoscrollingProtocol:(Protocol *)protocol
onClass:(Class)viewClass {
if (protocol == nil || viewClass == Nil) {
return NO;
}
if (class_conformsToProtocol(viewClass, protocol)) {
return YES;
}
return class_addProtocol(viewClass, protocol) &&
class_conformsToProtocol(viewClass, protocol);
}
@end
static void IDPrivateRequireTextInputTraitsSignature(
id target,
NSString *selectorName,
const char *returnType,
NSArray<NSString *> *argumentTypes) {
IDInteractionABISpec *spec =
IDInteractionABISpecMake(selectorName, returnType, argumentTypes);
Method method = class_getInstanceMethod(
object_getClass(target), NSSelectorFromString(selectorName));
NSMethodSignature *signature = method == NULL ? nil :
[NSMethodSignature signatureWithObjCTypes:method_getTypeEncoding(method)];
NSString *mismatch =
IDInteractionABIMismatchForSignature(signature, spec);
if (mismatch == nil) return;
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:[NSString stringWithFormat:
@"Text-input traits ABI mismatch: %@", mismatch]
userInfo:nil];
}
id IDPrivateDefaultTextInputTraits(void) {
Class traitsClass = NSClassFromString(@"UITextInputTraits");
SEL selector = NSSelectorFromString(TGPN_defaultTextInputTraits());
Method method = class_getClassMethod(traitsClass, selector);
NSMethodSignature *signature = method == NULL ? nil :
[NSMethodSignature signatureWithObjCTypes:method_getTypeEncoding(method)];
IDInteractionABISpec *spec =
IDInteractionABISpecMake(NSStringFromSelector(selector), @encode(id), @[]);
NSString *mismatch = IDInteractionABIMismatchForSignature(signature, spec);
if (mismatch != nil) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:[NSString stringWithFormat:
@"Text-input traits ABI mismatch: %@", mismatch]
userInfo:nil];
}
id traits = ((id (*)(id, SEL))objc_msgSend)(traitsClass, selector);
[IDPrivateUIKit requireObject:traits stage:@"Default text-input traits"];
return traits;
}
id IDPrivateInputTextInputTraits(id inputController) {
SEL selector = NSSelectorFromString(TGPN__textInputTraits());
IDPrivateRequireTextInputTraitsSignature(
inputController, NSStringFromSelector(selector), @encode(id), @[]);
id traits = ((id (*)(id, SEL))objc_msgSend)(inputController, selector);
[IDPrivateUIKit requireObject:traits stage:@"Input-controller text-input traits"];
return traits;
}
void IDPrivateSynchronizeTextInputTraits(
id inputController,
id<UITextInputTraits> source) {
id traits = IDPrivateInputTextInputTraits(inputController);
NSString *integerType = IDInteractionTypeEncoding(@encode(NSInteger));
NSString *unsignedIntegerType =
IDInteractionTypeEncoding(@encode(NSUInteger));
NSString *boolType = IDInteractionTypeEncoding(@encode(BOOL));
NSString *objectType = IDInteractionTypeEncoding(@encode(id));
for (NSString *selectorName in @[
@"setAutocapitalizationType:",
@"setAutocorrectionType:",
@"setSpellCheckingType:",
@"setSmartQuotesType:",
@"setSmartDashesType:",
@"setSmartInsertDeleteType:",
@"setInlinePredictionType:",
@"setMathExpressionCompletionType:",
@"setKeyboardType:",
@"setKeyboardAppearance:",
@"setReturnKeyType:",
@"setWritingToolsBehavior:",
]) {
IDPrivateRequireTextInputTraitsSignature(
traits, selectorName, @encode(void), @[integerType]);
}
for (NSString *selectorName in @[
@"setEnablesReturnKeyAutomatically:",
@"setSecureTextEntry:",
@"setAllowsNumberPadPopover:",
]) {
IDPrivateRequireTextInputTraitsSignature(
traits, selectorName, @encode(void), @[boolType]);
}
IDPrivateRequireTextInputTraitsSignature(
traits,
@"setAllowedWritingToolsResultOptions:",
@encode(void),
@[unsignedIntegerType]);
for (NSString *selectorName in @[
@"setTextContentType:",
@"setPasswordRules:",
@"setConversationContext:",
]) {
IDPrivateRequireTextInputTraitsSignature(
traits, selectorName, @encode(void), @[objectType]);
}
#define ID_SET_INTEGER_TRAIT(NAME, VALUE) \
((void (*)(id, SEL, NSInteger))objc_msgSend)( \
traits, NSSelectorFromString(@"set" NAME @":"), (NSInteger)(VALUE))
#define ID_SET_BOOL_TRAIT(NAME, VALUE) \
((void (*)(id, SEL, BOOL))objc_msgSend)( \
traits, NSSelectorFromString(@"set" NAME @":"), (BOOL)(VALUE))
#define ID_SET_OBJECT_TRAIT(NAME, VALUE) \
((void (*)(id, SEL, id))objc_msgSend)( \
traits, NSSelectorFromString(@"set" NAME @":"), (VALUE))
ID_SET_INTEGER_TRAIT("AutocapitalizationType", source.autocapitalizationType);
ID_SET_INTEGER_TRAIT("AutocorrectionType", source.autocorrectionType);
ID_SET_INTEGER_TRAIT("SpellCheckingType", source.spellCheckingType);
ID_SET_INTEGER_TRAIT("SmartQuotesType", source.smartQuotesType);
ID_SET_INTEGER_TRAIT("SmartDashesType", source.smartDashesType);
ID_SET_INTEGER_TRAIT("SmartInsertDeleteType", source.smartInsertDeleteType);
ID_SET_INTEGER_TRAIT("InlinePredictionType", source.inlinePredictionType);
ID_SET_INTEGER_TRAIT("MathExpressionCompletionType",
source.mathExpressionCompletionType);
ID_SET_INTEGER_TRAIT("KeyboardType", source.keyboardType);
ID_SET_INTEGER_TRAIT("KeyboardAppearance", source.keyboardAppearance);
ID_SET_INTEGER_TRAIT("ReturnKeyType", source.returnKeyType);
ID_SET_BOOL_TRAIT("EnablesReturnKeyAutomatically",
source.enablesReturnKeyAutomatically);
ID_SET_BOOL_TRAIT("SecureTextEntry", source.isSecureTextEntry);
ID_SET_OBJECT_TRAIT("TextContentType", source.textContentType);
ID_SET_OBJECT_TRAIT("PasswordRules", source.passwordRules);
ID_SET_INTEGER_TRAIT("WritingToolsBehavior", source.writingToolsBehavior);
((void (*)(id, SEL, NSUInteger))objc_msgSend)(
traits,
@selector(setAllowedWritingToolsResultOptions:),
source.allowedWritingToolsResultOptions);
ID_SET_OBJECT_TRAIT("ConversationContext", source.conversationContext);
ID_SET_BOOL_TRAIT("AllowsNumberPadPopover", source.allowsNumberPadPopover);
#undef ID_SET_INTEGER_TRAIT
#undef ID_SET_BOOL_TRAIT
#undef ID_SET_OBJECT_TRAIT
}
void IDPrivateInteractionSelectionChanged(id assistant) {
if ([assistant respondsToSelector:NSSelectorFromString(TGPN_selectionChanged())]) {
((void (*)(id, SEL))objc_msgSend)(assistant, NSSelectorFromString(TGPN_selectionChanged()));
}
}
void IDPrivateInteractionActivateSelection(id assistant) {
if ([assistant respondsToSelector:NSSelectorFromString(TGPN_activateSelection())]) {
((void (*)(id, SEL))objc_msgSend)(assistant, NSSelectorFromString(TGPN_activateSelection()));
}
}
void IDPrivateInteractionDeactivateSelection(id assistant) {
if ([assistant respondsToSelector:NSSelectorFromString(TGPN_deactivateSelection())]) {
((void (*)(id, SEL))objc_msgSend)(assistant, NSSelectorFromString(TGPN_deactivateSelection()));
}
}
void IDPrivateInteractionSetSelectionVisible(id assistant, BOOL visible) {
if ([assistant respondsToSelector:NSSelectorFromString(TGPN_setSelectionDisplayVisible_())]) {
((void (*)(id, SEL, BOOL))objc_msgSend)(
assistant,
NSSelectorFromString(TGPN_setSelectionDisplayVisible_()),
visible);
}
}
void IDPrivateInteractionSetNeedsSelectionDisplayUpdate(id assistant) {
((void (*)(id, SEL))objc_msgSend)(
assistant,
NSSelectorFromString(TGPN_setNeedsSelectionDisplayUpdate()));
}
void IDPrivateInteractionSetGestureRecognizers(id assistant) {
((void (*)(id, SEL))objc_msgSend)(assistant, NSSelectorFromString(TGPN_setGestureRecognizers()));
}
void IDPrivateInteractionClearGestureRecognizers(id assistant) {
((void (*)(id, SEL, BOOL))objc_msgSend)(
assistant,
NSSelectorFromString(TGPN_clearGestureRecognizers_()),
YES);
}
void IDPrivateInteractionClearGestureRecognizersNonforced(id assistant) {
((void (*)(id, SEL, BOOL))objc_msgSend)(
assistant,
NSSelectorFromString(TGPN_clearGestureRecognizers_()),
NO);
}
void IDPrivateInteractionResignedFirstResponder(id assistant) {
((void (*)(id, SEL))objc_msgSend)(
assistant, NSSelectorFromString(TGPN_resignedFirstResponder()));
}
void IDPrivateInteractionDetach(id assistant, BOOL force) {
((void (*)(id, SEL, BOOL))objc_msgSend)(
assistant, NSSelectorFromString(@"detach:"), force);
}
static BOOL IDPrivateTestingIvarHasObjectABI(Ivar ivar) {
if (ivar == NULL) return NO;
const char *type = ivar_getTypeEncoding(ivar);
if (type == NULL || type[0] != '@') return NO;
NSUInteger size = 0;
NSUInteger alignment = 0;
NSGetSizeAndAlignment(type, &size, &alignment);
ptrdiff_t offset = ivar_getOffset(ivar);
return size == sizeof(id) && alignment == __alignof__(id) && alignment > 0 && offset >= 0 &&
(NSUInteger)offset % alignment == 0;
}
static id IDPrivateTestingObjectIvar(id object, const char *name) {
if (object == nil) return nil;
Ivar ivar = class_getInstanceVariable(object_getClass(object), name);
if (!IDPrivateTestingIvarHasObjectABI(ivar)) return nil;
return object_getIvar(object, ivar);
}
static id IDPrivateTestingObjectIvarOfExactClass(
id object, const char *name, NSString *className) {
id value = IDPrivateTestingObjectIvar(object, name);
Class expectedClass = NSClassFromString(className);
return value != nil && expectedClass != Nil && [value class] == expectedClass
? value : nil;
}
static id IDPrivateTestingZeroArgumentObjectCall(id target, SEL selector,
BOOL classMethod) {
if (target == nil || ![target respondsToSelector:selector]) return nil;
Class cls = classMethod ? (Class)target : object_getClass(target);
Method method = classMethod ? class_getClassMethod(cls, selector) :
class_getInstanceMethod(cls, selector);
const char *encoding = method == NULL ? NULL : method_getTypeEncoding(method);
NSMethodSignature *signature = encoding == NULL ? nil :
[NSMethodSignature signatureWithObjCTypes:encoding];
if (signature == nil || signature.numberOfArguments != 2 ||
signature.methodReturnType[0] != '@') return nil;
return ((id (*)(id, SEL))objc_msgSend)(target, selector);
}
static NSString *IDPrivateTestingDelegateTarget(id owner, UIView *editor,
UIView *handoff) {
if (owner == nil) return @"nil";
if (owner == editor) return @"editor";
if (owner == handoff) return @"handoff";
for (Class cls = object_getClass(owner); cls != Nil; cls = class_getSuperclass(cls)) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList(cls, &count);
for (unsigned int index = 0; index < count; index += 1) {
if (!IDPrivateTestingIvarHasObjectABI(ivars[index])) continue;
id value = object_getIvar(owner, ivars[index]);
if (value == editor || value == handoff) {
free(ivars);
return value == editor ? @"editor-wrapper" : @"handoff-wrapper";
}
}
free(ivars);
}
return @"other";
}
NSDictionary<NSString *, id> *
IDPrivateCopyKeyboardInputDelegateOwnershipForUITesting(
UIView *editor, UIView *handoff) {
Class implementationClass = NSClassFromString(TGPN_UIKeyboardImpl());
id implementation = IDPrivateTestingZeroArgumentObjectCall(
implementationClass, NSSelectorFromString(TGPN_activeInstance()), YES);
id stateManager = IDPrivateTestingZeroArgumentObjectCall(
implementation, NSSelectorFromString(TGPN_keyboardStateManager()), NO);
id delegateManager = IDPrivateTestingZeroArgumentObjectCall(
stateManager, NSSelectorFromString(TGPN_inputDelegateManager()), NO);
BOOL valid = implementation != nil &&
[stateManager class] == NSClassFromString(TGPN__UIKeyboardStateManager()) &&
[delegateManager class] == NSClassFromString(TGPN_UIKBInputDelegateManager());
id key = valid ? IDPrivateTestingObjectIvar(delegateManager, TGPN__keyInputDelegate().UTF8String) : nil;
id selectable = valid ?
IDPrivateTestingObjectIvar(delegateManager, TGPN__selectableDelegate().UTF8String) : nil;
id async = valid ?
IDPrivateTestingObjectIvar(delegateManager, TGPN__asyncInputDelegate().UTF8String) : nil;
return @{
@"valid": @(valid),
@"keyInputTarget": IDPrivateTestingDelegateTarget(key, editor, handoff),
@"selectableTarget": IDPrivateTestingDelegateTarget(selectable, editor, handoff),
@"asyncTarget": IDPrivateTestingDelegateTarget(async, editor, handoff),
};
}
static BOOL IDPrivateTestingObjectHasLiveTimer(id object) {
for (Class cls = object_getClass(object); cls != Nil; cls = class_getSuperclass(cls)) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList(cls, &count);
for (unsigned int index = 0; index < count; index += 1) {
const char *name = ivar_getName(ivars[index]);
if (name != NULL && strstr(name, "timer") != NULL &&
IDPrivateTestingIvarHasObjectABI(ivars[index]) &&
object_getIvar(object, ivars[index]) != nil) {
free(ivars);
return YES;
}
}
free(ivars);
}
return NO;
}
NSDictionary<NSString *, id> *
IDPrivateTestingFloatingCursorLifecycleSnapshot(UIView *textInput) {
id assistant = nil;
SEL assistantSelector = NSSelectorFromString(TGPN_interactionAssistant());
if ([textInput respondsToSelector:assistantSelector]) {
assistant = ((id (*)(id, SEL))objc_msgSend)(textInput, assistantSelector);
}
id display = nil;
SEL displaySelector = NSSelectorFromString(TGPN__selectionViewManager());
if ([assistant respondsToSelector:displaySelector]) {
display = ((id (*)(id, SEL))objc_msgSend)(assistant, displaySelector);
}
id assistantSession = IDPrivateTestingObjectIvarOfExactClass(
assistant, TGPN__floatingCursorSession().UTF8String, TGPN__UITextFloatingCursorSession());
id displaySession = IDPrivateTestingObjectIvarOfExactClass(
display, TGPN__activeFloatingCursorSession().UTF8String, TGPN__UITextFloatingCursorSession());
id session = assistantSession ?: displaySession;
id autoscroll = IDPrivateTestingObjectIvar(textInput, TGPN__autoscroll().UTF8String);
if (autoscroll == nil) {
autoscroll = IDPrivateTestingObjectIvar(textInput, "_id_textAutoscroll");
}
UIView *container = nil;
SEL backendSelector = NSSelectorFromString(@"id_selectionPresentationBackend");
if ([textInput respondsToSelector:backendSelector]) {
id backend = ((id (*)(id, SEL))objc_msgSend)(textInput, backendSelector);
SEL existingSelector = NSSelectorFromString(@"existingSelectionContainerView");
if ([backend respondsToSelector:existingSelector]) {
container = ((id (*)(id, SEL))objc_msgSend)(backend, existingSelector);
}
} else {
SEL containerSelector = NSSelectorFromString(TGPN_selectionContainerView());
if ([textInput respondsToSelector:containerSelector]) {
container = ((id (*)(id, SEL))objc_msgSend)(textInput, containerSelector);
}
}
BOOL keyboardVisible = NO;
for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
if (![scene isKindOfClass:UIWindowScene.class]) continue;
for (UIWindow *candidateWindow in ((UIWindowScene *)scene).windows) {
NSString *className = NSStringFromClass(candidateWindow.class);
if ([className containsString:@"Keyboard"] && !candidateWindow.hidden &&
candidateWindow.alpha > 0.0) keyboardVisible = YES;
}
}
NSDictionary<NSString *, id> *recognizerState = IDPrivateRecognizerState(textInput);
UITextRange *range = [(id<UITextInput>)textInput selectedTextRange];
NSInteger location = NSNotFound;
NSInteger length = 0;
if (range != nil) {
id<UITextInput> input = (id<UITextInput>)textInput;
location = [input offsetFromPosition:input.beginningOfDocument toPosition:range.start];
length = [input offsetFromPosition:range.start toPosition:range.end];
}
return @{
@"selection": NSStringFromRange(location < 0 || length < 0
? NSMakeRange(NSNotFound, 0) : NSMakeRange((NSUInteger)location, (NSUInteger)length)),
@"firstResponder": @(textInput.isFirstResponder),
@"keyboardVisible": @(keyboardVisible),
@"window": @((BOOL)(textInput.window != nil)),
@"assistant": @((BOOL)(assistant != nil)),
@"session": @((BOOL)(session != nil)),
@"assistantSession": @((BOOL)(assistantSession != nil)),
@"displaySession": @((BOOL)(displaySession != nil)),
@"sessionOwnersMatch": @((BOOL)(assistantSession != nil &&
assistantSession == displaySession)),
@"sessionClass": session == nil ? @"nil" : NSStringFromClass([session class]),
@"autoscroll": @((BOOL)(autoscroll != nil)),
@"autoscrollActive": @(IDPrivateTestingObjectHasLiveTimer(autoscroll)),
@"container": @((BOOL)(container != nil)),
@"containerToken": container == nil ? @"nil" :
[NSString stringWithFormat:@"%p", container],
@"containerSubviews": @(container.subviews.count),
@"gestures": @(textInput.gestureRecognizers.count),
@"recognizerCount": recognizerState[@"recognizerCount"],
@"recognizerSignature": recognizerState[@"recognizerSignature"],
@"recognizerBaselineCount": recognizerState[@"recognizerBaselineCount"],
@"recognizerUnexpectedNewCount": recognizerState[@"recognizerUnexpectedNewCount"],
@"recognizerSubsetOfBaseline": recognizerState[@"recognizerSubsetOfBaseline"],
@"recognizerBaselineIdentitySignature": recognizerState[@"recognizerBaselineIdentitySignature"],
@"recognizerIdentitySignature": recognizerState[@"recognizerIdentitySignature"],
@"editable": @([(id)textInput isEditable]),
@"selectable": @([(id)textInput isSelectable]),
};
}
static NSString *IDPrivatePresentationFingerprint(UIView *container) {
if (container == nil) return @"nil";
NSMutableArray<NSString *> *parts = [NSMutableArray array];
NSMutableArray<UIView *> *pending = [NSMutableArray arrayWithObject:container];
while (pending.count > 0) {
UIView *view = pending.lastObject;
[pending removeLastObject];
[pending addObjectsFromArray:view.subviews];
[parts addObject:[NSString stringWithFormat:@"%@|%@|%d|%.4f",
NSStringFromClass(view.class), NSStringFromCGRect(view.frame),
view.hidden, view.alpha]];
}
return [parts componentsJoinedByString:@";"];
}
static NSUInteger IDPrivateObservedPresentationUpdates(UIView *editor, UIView *container) {
NSString *fingerprint = IDPrivatePresentationFingerprint(container);
NSDictionary *old = objc_getAssociatedObject(editor, IDPrivateStockPresentationKey);
NSUInteger count = [old[@"count"] unsignedIntegerValue];
if (![old[@"fingerprint"] isEqualToString:fingerprint]) count += 1;
objc_setAssociatedObject(editor, IDPrivateStockPresentationKey,
@{@"count": @(count), @"fingerprint": fingerprint},
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return count;
}
static BOOL IDPrivateTestingBoolIvar(id object, const char *name, BOOL *value) {
if (object == nil) return NO;
Ivar ivar = class_getInstanceVariable(object_getClass(object), name);
if (ivar == NULL || strcmp(ivar_getTypeEncoding(ivar), @encode(BOOL)) != 0) return NO;
ptrdiff_t offset = ivar_getOffset(ivar);
if (offset < 0) return NO;
if (value != NULL) *value = *(BOOL *)((uint8_t *)(__bridge void *)object + offset);
return YES;
}
static id IDPrivateStockAssistant(UIView *editor) {
SEL selector = NSSelectorFromString(TGPN_interactionAssistant());
return [editor respondsToSelector:selector]
? ((id (*)(id, SEL))objc_msgSend)(editor, selector) : nil;
}
static id IDPrivateStockFloatingSession(UIView *editor) {
id assistant = IDPrivateStockAssistant(editor);
id session = IDPrivateTestingObjectIvarOfExactClass(
assistant, TGPN__floatingCursorSession().UTF8String, TGPN__UITextFloatingCursorSession());
if (session != nil) return session;
SEL displaySelector = NSSelectorFromString(TGPN__selectionViewManager());
id display = [assistant respondsToSelector:displaySelector]
? ((id (*)(id, SEL))objc_msgSend)(assistant, displaySelector) : nil;
return IDPrivateTestingObjectIvarOfExactClass(
display, TGPN__activeFloatingCursorSession().UTF8String, TGPN__UITextFloatingCursorSession());
}
static id IDPrivateStockAutoscroll(UIView *editor) {
id assistant = IDPrivateStockAssistant(editor);
for (id owner in @[editor, assistant ?: NSNull.null]) {
if (owner == NSNull.null) continue;
id value = IDPrivateTestingObjectIvar(owner, TGPN__autoscroll().UTF8String);
if (value == nil) value = IDPrivateTestingObjectIvar(owner, "_textAutoscroll");
if (value == nil) value = IDPrivateTestingObjectIvar(owner, "_id_textAutoscroll");
if (value != nil) return value;
for (Class cls = object_getClass(owner); cls != Nil; cls = class_getSuperclass(cls)) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList(cls, &count);
for (unsigned int index = 0; index < count; index += 1) {
const char *type = ivar_getTypeEncoding(ivars[index]);
if (type == NULL || type[0] != '@') continue;
id candidate = object_getIvar(owner, ivars[index]);
if ([NSStringFromClass([candidate class]) containsString:@"Autoscroll"]) {
free(ivars);
return candidate;
}
}
free(ivars);
}
}
return nil;
}
static UIView *IDPrivateStockSelectionContainer(UIView *editor) {
SEL selector = NSSelectorFromString(TGPN_selectionContainerView());
return [editor respondsToSelector:selector]
? ((UIView *(*)(id, SEL))objc_msgSend)(editor, selector) : nil;
}
@interface IDPrivateFloatingContainerIdentityTracker : NSObject
@property(nonatomic, strong) NSMutableSet<NSNumber *> *seenIdentities;
@end
@implementation IDPrivateFloatingContainerIdentityTracker
- (instancetype)init {
self = [super init];
if (self != nil) _seenIdentities = [NSMutableSet set];
return self;
}
@end
static NSNumber *IDPrivateFloatingContainerIdentityToken(UIView *container) {
@synchronized (IDPrivateFloatingContainerIdentityTracker.class) {
id existing = objc_getAssociatedObject(
container, IDPrivateFloatingContainerIdentityTokenKey);
if (existing != nil) {
if (![existing isKindOfClass:NSNumber.class] ||
[existing unsignedIntegerValue] == 0) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"Floating container identity token has incompatible type"
userInfo:nil];
}
return existing;
}
if (IDPrivateNextFloatingContainerIdentityToken == NSUIntegerMax) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"Floating container identity token sequence exhausted"
userInfo:nil];
}
NSNumber *token = @(++IDPrivateNextFloatingContainerIdentityToken);
objc_setAssociatedObject(container, IDPrivateFloatingContainerIdentityTokenKey,
token, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return token;
}
}
static BOOL IDPrivateNoArgumentSelectorHasReturnType(id target, SEL selector,
const char *returnType) {
NSMethodSignature *signature = [target methodSignatureForSelector:selector];
return signature != nil && signature.numberOfArguments == 2 &&
IDInteractionTypeMatches(signature.methodReturnType, returnType);
}
NSUInteger IDPrivateFloatingContainerCreationCountForUITesting(UIView *textInput) {
if (textInput == nil) return 0;
SEL backendSelector = NSSelectorFromString(@"id_selectionPresentationBackend");
if ([textInput respondsToSelector:backendSelector]) {
if (!IDPrivateNoArgumentSelectorHasReturnType(textInput, backendSelector, @encode(id))) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"Floating container backend accessor has incompatible ABI"
userInfo:nil];
}
id backend = ((id (*)(id, SEL))objc_msgSend)(textInput, backendSelector);
SEL countSelector = NSSelectorFromString(@"selectionContainerCreationCount");
if (!IDPrivateNoArgumentSelectorHasReturnType(
backend, countSelector, @encode(NSUInteger))) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"Floating container creation count has incompatible ABI"
userInfo:nil];
}
return ((NSUInteger (*)(id, SEL))objc_msgSend)(backend, countSelector);
}
SEL containerSelector = NSSelectorFromString(TGPN_selectionContainerView());
if (![textInput respondsToSelector:containerSelector]) return 0;
if (!IDPrivateNoArgumentSelectorHasReturnType(
textInput, containerSelector, @encode(id))) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"Stock floating container accessor has incompatible ABI"
userInfo:nil];
}
IDPrivateFloatingContainerIdentityTracker *tracker = objc_getAssociatedObject(
textInput, IDPrivateFloatingContainerIdentityTrackerKey);
UIView *container = IDPrivateStockSelectionContainer(textInput);
if (container == nil) return tracker.seenIdentities.count;
if (tracker == nil) {
tracker = [IDPrivateFloatingContainerIdentityTracker new];
objc_setAssociatedObject(textInput, IDPrivateFloatingContainerIdentityTrackerKey,
tracker, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
NSNumber *token = IDPrivateFloatingContainerIdentityToken(container);
@synchronized (tracker) {
[tracker.seenIdentities addObject:token];
return tracker.seenIdentities.count;
}
}
@interface IDPrivateFloatingLifecycleWeakReferences ()
@property(nonatomic, weak) UIView *editor;
@property(nonatomic, weak) id layoutController;
@property(nonatomic, weak) id assistant;
@property(nonatomic, weak) UIView *container;
@property(nonatomic, weak) id session;
@property(nonatomic, weak) id autoscroll;
@property(nonatomic) NSUInteger observationSequence;
@property(nonatomic) CFTimeInterval releaseEpoch;
@property(nonatomic) CFTimeInterval actionEpoch;
@property(nonatomic) CFTimeInterval terminalEpoch;
@property(nonatomic) CFTimeInterval liftTargetEpoch;
@property(nonatomic) CFTimeInterval captureTargetEpoch;
@property(nonatomic, copy) NSString *observationPhase;
@property(nonatomic, copy) NSString *terminalEvent;
@property(nonatomic) NSUInteger terminalCountAtRelease;
@property(nonatomic, copy) NSDictionary<NSString *, id> *terminalSnapshot;
@property(nonatomic, copy) NSDictionary<NSString *, id> *heldSnapshot;
@property(nonatomic, copy) NSDictionary<NSString *, id> *immediateSnapshot;
@property(nonatomic, copy) NSDictionary<NSString *, id> *exactSnapshot;
@property(nonatomic, copy) NSDictionary<NSString *, id> *postLiftSnapshot;
@property(nonatomic) BOOL keyboardVisible;
@end
@implementation IDPrivateFloatingLifecycleWeakReferences
// Not the default `_autoscroll`: that is a private UIKit ivar name read above, and it must not reach
// the binary as text (`TGPrivateRuntimeNames.h`).
@synthesize autoscroll = _id_autoscroll;
- (instancetype)initWithEditor:(UIView *)editor {
return [self initWithEditor:editor
layoutController:IDPrivateTestingInternalTextLayoutController(editor)];
}
- (instancetype)initWithEditor:(UIView *)editor
layoutController:(id)layoutController {
self = [super init];
if (self != nil) {
[self refreshWithEditor:editor layoutController:layoutController];
[NSNotificationCenter.defaultCenter addObserver:self
selector:@selector(id_keyboardWillShow:)
name:UIKeyboardWillShowNotification object:nil];
[NSNotificationCenter.defaultCenter addObserver:self
selector:@selector(id_keyboardDidHide:)
name:UIKeyboardDidHideNotification object:nil];
}
return self;
}
- (void)dealloc {
[NSNotificationCenter.defaultCenter removeObserver:self];
}
- (void)id_keyboardWillShow:(NSNotification *)notification {
(void)notification;
self.keyboardVisible = YES;
}
- (void)id_keyboardDidHide:(NSNotification *)notification {
(void)notification;
self.keyboardVisible = NO;
}
- (void)refreshWithEditor:(UIView *)editor {
[self refreshWithEditor:editor
layoutController:IDPrivateTestingInternalTextLayoutController(editor)];
}
- (void)refreshWithEditor:(UIView *)editor
layoutController:(id)layoutController {
self.editor = editor;
self.layoutController = layoutController;
self.assistant = IDPrivateStockAssistant(editor);
self.container = IDPrivateStockSelectionContainer(editor);
id session = IDPrivateStockFloatingSession(editor);
id autoscroll = IDPrivateStockAutoscroll(editor);
if (session != nil) self.session = session;
if (autoscroll != nil) self.autoscroll = autoscroll;
}
- (NSDictionary<NSString *, NSNumber *> *)copyRetentionState {
return @{
@"editorRetained": @((BOOL)(self.editor != nil)),
@"assistantRetained": @((BOOL)(self.assistant != nil)),
@"containerRetained": @((BOOL)(self.container != nil)),
@"sessionRetained": @((BOOL)(self.session != nil)),
@"autoscrollRetained": @((BOOL)(self.autoscroll != nil)),
};
}
- (NSDictionary<NSString *, id> *)copyAtomicFloatingSnapshot {
UIView *editor = self.editor;
self.observationSequence += 1;
NSUInteger sequence = self.observationSequence;
NSDictionary *lifecycle = editor == nil ? @{} :
IDPrivateCopyFloatingLifecycleStateWithLayoutControllerForUITesting(
editor, self.layoutController);
id<UITextInput> input = (id<UITextInput>)editor;
UITextRange *range = input.selectedTextRange;
NSInteger location = NSNotFound;
NSInteger length = 0;
if (range != nil) {
location = [input offsetFromPosition:input.beginningOfDocument toPosition:range.start];
length = [input offsetFromPosition:range.start toPosition:range.end];
}
NSRange selection = location < 0 || length < 0 ? NSMakeRange(NSNotFound, 0) :
NSMakeRange((NSUInteger)location, (NSUInteger)length);
CGPoint offset = [editor isKindOfClass:UIScrollView.class]
? ((UIScrollView *)editor).contentOffset : CGPointZero;
return @{
@"selection": @{@"location": @(selection.location), @"length": @(selection.length)},
@"startAffinity": lifecycle[@"startAffinity"] ?: @0,
@"endAffinity": lifecycle[@"endAffinity"] ?: @0,
@"firstResponder": @((BOOL)editor.isFirstResponder),
@"keyboardVisible": @(self.keyboardVisible),
@"containerCreations":
@(IDPrivateFloatingContainerCreationCountForUITesting(editor)),
@"containerStable": lifecycle[@"containerStable"] ?: @NO,
@"presentationUpdates": lifecycle[@"presentationUpdates"] ?: @0,
@"contentOffset": @{@"x": @(offset.x), @"y": @(offset.y)},
@"lifecycle": lifecycle,
@"retention": [self copyRetentionState],
@"observation": @{
@"source": @"app-side-weak-reference-snapshot",
@"sequence": @(sequence), @"lifecycleSequence": @(sequence),
@"retentionSequence": @(sequence), @"capturedAt": @(CACurrentMediaTime()),
@"actionEpoch": @(self.actionEpoch),
@"releaseEpoch": @(self.releaseEpoch),
@"terminalEpoch": @(self.terminalEpoch),
@"liftTargetEpoch": @(self.liftTargetEpoch),
@"captureTargetEpoch": @(self.captureTargetEpoch),
@"phase": self.observationPhase ?: @"live",
@"terminalEvent": self.terminalEvent ?: @"none",
@"terminalCountAtRelease": @(self.terminalCountAtRelease),
},
};
}
- (void)markReleaseAttemptAfterTerminalEvent:(NSString *)event
count:(NSUInteger)count
terminalEpoch:(CFTimeInterval)terminalEpoch
liftTargetEpoch:(CFTimeInterval)liftTargetEpoch {
[self markTerminalObservationForEvent:event count:count
terminalEpoch:terminalEpoch liftTargetEpoch:liftTargetEpoch];
self.releaseEpoch = CACurrentMediaTime();
self.captureTargetEpoch = self.releaseEpoch;
self.observationPhase = @"terminal";
self.terminalSnapshot = [self copyAtomicFloatingSnapshot];
}
- (void)markTerminalObservationForEvent:(NSString *)event
count:(NSUInteger)count
terminalEpoch:(CFTimeInterval)terminalEpoch
liftTargetEpoch:(CFTimeInterval)liftTargetEpoch {
self.terminalEvent = [event copy];
self.terminalCountAtRelease = count;
self.terminalEpoch = terminalEpoch;
if (self.liftTargetEpoch == 0) self.liftTargetEpoch = liftTargetEpoch;
self.captureTargetEpoch = terminalEpoch;
self.observationPhase = @"terminal";
self.terminalSnapshot = [self copyAtomicFloatingSnapshot];
}
- (void)markActionObservationAtEpoch:(CFTimeInterval)actionEpoch
liftDelay:(NSTimeInterval)liftDelay {
self.actionEpoch = actionEpoch;
self.liftTargetEpoch = actionEpoch + liftDelay;
}
- (void)recordImmediateReleaseSnapshot {
self.captureTargetEpoch = self.releaseEpoch > 0 ? self.releaseEpoch :
(self.actionEpoch > 0 ? self.actionEpoch : self.terminalEpoch);
self.observationPhase = @"immediate";
self.immediateSnapshot = [self copyAtomicFloatingSnapshot];
}
- (void)beginHeldSnapshotPollingForTerminalEvent:(NSString *)event {
UIView *armedEditor = self.editor;
NSString *countKey = [event stringByAppendingString:@"Count"];
NSDictionary *armedLifecycle = armedEditor == nil ? @{} :
IDPrivateCopyFloatingLifecycleStateWithLayoutControllerForUITesting(
armedEditor, self.layoutController);
NSUInteger terminalCountAtArm = [armedLifecycle[countKey] unsignedIntegerValue];
__weak typeof(self) weakSelf = self;
__block void (^poll)(void) = nil;
poll = ^{
typeof(self) self = weakSelf;
if (self == nil || self.heldSnapshot != nil || self.releaseEpoch > 0) {
poll = nil;
return;
}
UIView *editor = self.editor;
NSDictionary *lifecycle = editor == nil ? @{} :
IDPrivateCopyFloatingLifecycleStateWithLayoutControllerForUITesting(
editor, self.layoutController);
BOOL held = editor.isFirstResponder && self.keyboardVisible &&
[lifecycle[@"sessionActive"] boolValue] &&
[lifecycle[countKey] unsignedIntegerValue] == terminalCountAtArm &&
[lifecycle[@"containerStable"] boolValue];
if (held) {
IDPrivateFreezeRecognizerBaselineAtHeldCapture(editor);
[self refreshWithEditor:editor
layoutController:self.layoutController];
self.captureTargetEpoch = CACurrentMediaTime();
self.observationPhase = @"held";
self.heldSnapshot = [self copyAtomicFloatingSnapshot];
poll = nil;
return;
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_MSEC),
dispatch_get_main_queue(), poll);
};
dispatch_async(dispatch_get_main_queue(), poll);
}
- (void)id_capturePostLiftSnapshotAtTarget:(CFTimeInterval)targetEpoch {
self.captureTargetEpoch = targetEpoch;
self.observationPhase = @"post-keyboard-lift-500ms";
self.postLiftSnapshot = [self copyAtomicFloatingSnapshot];
}
- (void)scheduleReleaseSnapshots {
CFTimeInterval baseEpoch = self.releaseEpoch > 0 ? self.releaseEpoch :
(self.actionEpoch > 0 ? self.actionEpoch : self.terminalEpoch);
CFTimeInterval exactTargetEpoch = baseEpoch + 0.500;
CFTimeInterval postTargetEpoch = self.liftTargetEpoch + 0.500;
BOOL coalescesEqualTargets = exactTargetEpoch == postTargetEpoch;
CFTimeInterval exactDelay = MAX(exactTargetEpoch - CACurrentMediaTime(), 0);
NSTimer *exactTimer = [[NSTimer alloc]
initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:exactDelay]
interval:0 repeats:NO block:^(__unused NSTimer *timer) {
self.captureTargetEpoch = exactTargetEpoch;
self.observationPhase = self.releaseEpoch > 0 ? @"exact-release-500ms" :
(self.actionEpoch > 0 ? @"exact-action-500ms" : @"exact-terminal-500ms");
self.exactSnapshot = [self copyAtomicFloatingSnapshot];
if (coalescesEqualTargets) {
[self id_capturePostLiftSnapshotAtTarget:postTargetEpoch];
}
}];
[NSRunLoop.mainRunLoop addTimer:exactTimer forMode:NSRunLoopCommonModes];
if (coalescesEqualTargets) return;
CFTimeInterval postDelay = MAX(postTargetEpoch - CACurrentMediaTime(), 0);
NSTimer *postTimer = [[NSTimer alloc]
initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:postDelay]
interval:0 repeats:NO block:^(__unused NSTimer *timer) {
[self id_capturePostLiftSnapshotAtTarget:postTargetEpoch];
}];
[NSRunLoop.mainRunLoop addTimer:postTimer forMode:NSRunLoopCommonModes];
}
- (NSDictionary<NSString *, id> *)copyTerminalSnapshot {
return [self.terminalSnapshot copy];
}
- (NSDictionary<NSString *, id> *)copyReleaseSnapshotForPhase:(NSString *)phase {
if ([phase isEqualToString:@"held"]) return [self.heldSnapshot copy];
if ([phase isEqualToString:@"immediate"]) return [self.immediateSnapshot copy];
if ([phase isEqualToString:@"exact-release-500ms"]) return [self.exactSnapshot copy];
if ([phase isEqualToString:@"post-keyboard-lift-500ms"])
return [self.postLiftSnapshot copy];
return nil;
}
@end
static NSDictionary<NSString *, id> *
IDPrivateCopyFloatingLifecycleStateWithController(UIView *editor,
id controller) {
UIView *firstContainer = IDPrivateStockSelectionContainer(editor);
UIView *secondContainer = IDPrivateStockSelectionContainer(editor);
id assistant = IDPrivateStockAssistant(editor);
SEL displaySelector = NSSelectorFromString(TGPN__selectionViewManager());
id display = [assistant respondsToSelector:displaySelector]
? ((id (*)(id, SEL))objc_msgSend)(assistant, displaySelector) : nil;
id assistantSession = IDPrivateTestingObjectIvarOfExactClass(
assistant, TGPN__floatingCursorSession().UTF8String, TGPN__UITextFloatingCursorSession());
id displaySession = IDPrivateTestingObjectIvarOfExactClass(
display, TGPN__activeFloatingCursorSession().UTF8String, TGPN__UITextFloatingCursorSession());
id session = assistantSession ?: displaySession;
BOOL sessionActive = NO;
(void)IDPrivateTestingBoolIvar(session, TGPN__isValid().UTF8String, &sessionActive);
id autoscroll = IDPrivateStockAutoscroll(editor);
NSDictionary<NSString *, id> *recognizerState = IDPrivateRecognizerState(editor);
NSMutableDictionary *trace = [editor isKindOfClass:UIScrollView.class]
? [IDPrivateCopyTextAutoscrollTraceForUITesting((UIScrollView *)editor) mutableCopy]
: [NSMutableDictionary dictionary];
id<UITextInput> input = (id<UITextInput>)editor;
UITextRange *range = input.selectedTextRange;
NSInteger startAffinity = UITextStorageDirectionForward;
NSInteger endAffinity = UITextStorageDirectionForward;
SEL affinitySelector = NSSelectorFromString(TGPN_affinityForPosition_());
if (range != nil && [controller respondsToSelector:affinitySelector]) {
startAffinity = ((NSInteger (*)(id, SEL, id))objc_msgSend)(
controller, affinitySelector, range.start);
endAffinity = ((NSInteger (*)(id, SEL, id))objc_msgSend)(
controller, affinitySelector, range.end);
}
return @{
@"normalEndCount": trace[@"normalEndCount"] ?: @0,
@"windowDetachActionCount": trace[@"windowDetachActionCount"] ?: @0,
@"windowDetachCount": trace[@"windowDetachCount"] ?: @0,
@"resignActionCount": trace[@"resignActionCount"] ?: @0,
@"keyboardDismissActionCount": trace[@"keyboardDismissActionCount"] ?: @0,
@"editableOffActionCount": trace[@"editableOffActionCount"] ?: @0,
@"selectableOffActionCount": trace[@"selectableOffActionCount"] ?: @0,
@"responderLossCount": trace[@"responderLossCount"] ?: @0,
@"editableOffCount": trace[@"editableOffCount"] ?: @0,
@"selectableOffCount": trace[@"selectableOffCount"] ?: @0,
@"startCount": trace[@"startCount"] ?: @0,
@"cancelCount": trace[@"cancelCount"] ?: @0,
@"presentationUpdates": @(IDPrivateObservedPresentationUpdates(editor, firstContainer)),
@"containerToken": firstContainer == nil ? @"nil" :
[NSString stringWithFormat:@"%p", firstContainer],
@"containerStable": @((BOOL)(firstContainer != nil &&
firstContainer == secondContainer && firstContainer.superview != nil)),
@"containerSubviews": @(firstContainer.subviews.count),
@"sessionObject": @((BOOL)(session != nil)),
@"assistantSessionObject": @((BOOL)(assistantSession != nil)),
@"displaySessionObject": @((BOOL)(displaySession != nil)),
@"sessionOwnersMatch": @((BOOL)(assistantSession != nil &&
assistantSession == displaySession)),
@"assistantSessionToken": assistantSession == nil ? @"nil" :
[NSString stringWithFormat:@"%p", assistantSession],
@"displaySessionToken": displaySession == nil ? @"nil" :
[NSString stringWithFormat:@"%p", displaySession],
@"sessionActive": @(sessionActive),
@"autoscrollObject": @((BOOL)(autoscroll != nil)),
@"autoscrollActive": @(IDPrivateTestingObjectHasLiveTimer(autoscroll)),
@"recognizerCount": recognizerState[@"recognizerCount"],
@"recognizerSignature": recognizerState[@"recognizerSignature"],
@"recognizerBaselineCount": recognizerState[@"recognizerBaselineCount"],
@"recognizerUnexpectedNewCount": recognizerState[@"recognizerUnexpectedNewCount"],
@"recognizerSubsetOfBaseline": recognizerState[@"recognizerSubsetOfBaseline"],
@"recognizerBaselineIdentitySignature": recognizerState[@"recognizerBaselineIdentitySignature"],
@"recognizerIdentitySignature": recognizerState[@"recognizerIdentitySignature"],
@"editable": @([(id)editor isEditable]),
@"selectable": @([(id)editor isSelectable]),
@"startAffinity": @(startAffinity), @"endAffinity": @(endAffinity),
};
}
NSDictionary<NSString *, id> *
IDPrivateCopyFloatingLifecycleStateWithLayoutControllerForUITesting(
UIView *editor,
id layoutController) {
if (layoutController == nil) {
@throw [NSException exceptionWithName:NSInvalidArgumentException
reason:@"Floating lifecycle diagnostics require an explicit layout controller"
userInfo:nil];
}
return IDPrivateCopyFloatingLifecycleStateWithController(
editor, layoutController);
}
NSDictionary<NSString *, id> *
IDPrivateCopyStockFloatingLifecycleStateForUITesting(UIView *editor) {
return IDPrivateCopyFloatingLifecycleStateWithController(
editor, IDPrivateTestingInternalTextLayoutController(editor));
}
NSDictionary<NSString *, id> *
IDPrivateCopyFloatingLifecycleStateForUITesting(UIView *editor) {
return IDPrivateCopyStockFloatingLifecycleStateForUITesting(editor);
}
void IDPrivateTestingRequestResponderLoss(UIView *view) {
if ([view isKindOfClass:UIScrollView.class]) {
IDPrivateRecordFloatingLifecycleEventForUITesting(
(UIScrollView *)view, @"resignAction");
}
[view resignFirstResponder];
}
void IDPrivateTestingRequestKeyboardDismissal(UIView *view) {
if ([view isKindOfClass:UIScrollView.class]) {
IDPrivateRecordFloatingLifecycleEventForUITesting(
(UIScrollView *)view, @"keyboardDismissAction");
}
[view endEditing:YES];
}
static void IDPrivateTestingRequestBooleanOff(UIView *view,
NSString *event,
BOOL selectable) {
NSString *countKey = [event stringByAppendingString:@"Count"];
NSUInteger before = [IDPrivateCopyTextAutoscrollTraceForUITesting(
(UIScrollView *)view)[countKey] unsignedIntegerValue];
IDPrivateRecordFloatingLifecycleEventForUITesting((UIScrollView *)view,
[event stringByAppendingString:@"Action"]);
if (selectable) [(id)view setSelectable:NO];
else [(id)view setEditable:NO];
NSDictionary *afterTrace = IDPrivateCopyTextAutoscrollTraceForUITesting(
(UIScrollView *)view);
BOOL changedOff = selectable ? ![(id)view isSelectable] : ![(id)view isEditable];
if (changedOff && [afterTrace[countKey] unsignedIntegerValue] == before) {
IDPrivateRecordFloatingLifecycleEventForUITesting(
(UIScrollView *)view, event);
}
}
void IDPrivateTestingRequestEditableOff(UIView *view) {
IDPrivateTestingRequestBooleanOff(view, @"editableOff", NO);
}
void IDPrivateTestingRequestSelectableOff(UIView *view) {
IDPrivateTestingRequestBooleanOff(view, @"selectableOff", YES);
}
void IDPrivateTestingRequestWindowDetach(UIView *view) {
if ([view isKindOfClass:UIScrollView.class]) {
IDPrivateRecordFloatingLifecycleEventForUITesting(
(UIScrollView *)view, @"windowDetachAction");
}
[view removeFromSuperview];
if ([view isKindOfClass:UIScrollView.class]) {
IDPrivateRecordFloatingLifecycleEventForUITesting(
(UIScrollView *)view, @"windowDetach");
}
}
NSArray<NSDictionary<NSString *, NSNumber *> *> *
IDPrivateCopyStockVisibleSelectionHandlesForUITesting(UIView *root) {
NSMutableArray<NSDictionary<NSString *, NSNumber *> *> *result = [NSMutableArray array];
NSMutableArray<UIView *> *pending = [NSMutableArray arrayWithObject:root];
while (pending.count > 0) {
UIView *view = pending.lastObject;
[pending removeLastObject];
[pending addObjectsFromArray:view.subviews];
if (![NSStringFromClass(view.class) containsString:TGPN__UITextSelectionLollipopView()] ||
view.hidden || view.alpha <= 0.01) continue;
CGRect rect = IDPrivateVisibleDescendantBoundsInView(view, root);
if (CGRectIsNull(rect) || CGRectIsEmpty(rect)) continue;
[result addObject:IDPrivateRectJSON(rect)];
}
return result.copy;
}
void IDPrivateInteractionBeginFloatingCursor(id assistant, CGPoint point) {
((void (*)(id, SEL, CGPoint))objc_msgSend)(
assistant,
NSSelectorFromString(@"beginFloatingCursorAtPoint:"),
point);
}
void IDPrivateInteractionUpdateFloatingCursor(id assistant, CGPoint point, BOOL animated) {
((void (*)(id, SEL, CGPoint, BOOL))objc_msgSend)(
assistant,
NSSelectorFromString(@"updateFloatingCursorAtPoint:animated:"),
point,
animated);
}
void IDPrivateInteractionEndFloatingCursor(id assistant) {
((void (*)(id, SEL))objc_msgSend)(assistant, NSSelectorFromString(@"endFloatingCursor"));
}
void IDPrivateInteractionSelectWord(id assistant) {
((void (*)(id, SEL))objc_msgSend)(assistant, NSSelectorFromString(TGPN_selectWord()));
}
void IDPrivateInteractionSelectAll(id assistant, id sender) {
((void (*)(id, SEL, id))objc_msgSend)(
assistant,
NSSelectorFromString(@"selectAll:"),
sender);
}
void IDPrivateInteractionAutoscrollWillNotStart(id assistant) {
SEL selector = NSSelectorFromString(TGPN_autoscrollWillNotStart());
if ([assistant respondsToSelector:selector]) {
((void (*)(id, SEL))objc_msgSend)(assistant, selector);
}
}
id IDPrivateNewTextAutoscroll(void) {
Class cls = NSClassFromString(TGPN_UIAutoscroll());
if (cls == Nil) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:[TGPN_UIAutoscroll() stringByAppendingString:@" is unavailable"]
userInfo:nil];
}
id autoscroll = [cls new];
NSArray<IDInteractionABISpec *> *specs = @[
IDInteractionABISpecMake(
TGPN_startAutoscroll_scrollContainer_point_directions_repeatInterval_(),
@encode(BOOL),
@[IDInteractionTypeEncoding(@encode(id)),
IDInteractionTypeEncoding(@encode(id)),
IDInteractionTypeEncoding(@encode(CGPoint)),
IDInteractionTypeEncoding(@encode(int)),
IDInteractionTypeEncoding(@encode(double))]),
IDInteractionABISpecMake(@"invalidate", @encode(void), @[]),
];
NSArray<NSString *> *mismatches = IDInteractionABIMismatches(autoscroll, specs);
if (mismatches.count != 0) {
NSString *reason = [NSString stringWithFormat:
@"%@ ABI mismatch:\n%@", TGPN_UIAutoscroll(),
[mismatches componentsJoinedByString:@"\n"]];
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:reason
userInfo:nil];
}
return autoscroll;
}
BOOL IDPrivateTextAutoscrollStart(id autoscroll,
id target,
UIScrollView *scrollContainer,
CGPoint point,
NSUInteger directions,
NSTimeInterval repeatInterval) {
return ((BOOL (*)(id, SEL, id, id, CGPoint, int, double))objc_msgSend)(
autoscroll,
NSSelectorFromString(
TGPN_startAutoscroll_scrollContainer_point_directions_repeatInterval_()),
target,
scrollContainer,
point,
(int)directions,
repeatInterval);
}
void IDPrivateTextAutoscrollInvalidate(id autoscroll) {
if (autoscroll != nil) {
((void (*)(id, SEL))objc_msgSend)(autoscroll, NSSelectorFromString(@"invalidate"));
}
}
static void IDPrivateTestingRequireUIKitAutoscrollGetter(
id autoscroll,
NSString *selectorName,
const char *returnType) {
SEL selector = NSSelectorFromString(selectorName);
NSMethodSignature *signature =
[autoscroll methodSignatureForSelector:selector];
IDInteractionABISpec *spec =
IDInteractionABISpecMake(selectorName, returnType, @[]);
NSString *mismatch =
IDInteractionABIMismatchForSignature(signature, spec);
if (mismatch == nil) return;
@throw [NSException
exceptionWithName:NSInternalInconsistencyException
reason:[NSString stringWithFormat:
@"UIKit autoscroll oracle ABI mismatch: %@", mismatch]
userInfo:nil];
}
static id IDPrivateTestingUIKitAutoscrollTimer(id autoscroll) {
for (Class cls = object_getClass(autoscroll);
cls != Nil;
cls = class_getSuperclass(cls)) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList(cls, &count);
for (unsigned int index = 0; index < count; index += 1) {
const char *name = ivar_getName(ivars[index]);
const char *type = ivar_getTypeEncoding(ivars[index]);
if (name != NULL && strstr(name, "timer") != NULL &&
type != NULL && type[0] == '@') {
id timer = object_getIvar(autoscroll, ivars[index]);
free(ivars);
return timer;
}
}
free(ivars);
}
return nil;
}
id IDPrivateTestingNewUIKitAutoscroll(void) {
return IDPrivateNewTextAutoscroll();
}
BOOL IDPrivateTestingStartUIKitAutoscroll(
id autoscroll,
id target,
UIScrollView *scrollContainer,
CGPoint point,
NSUInteger directions,
NSTimeInterval repeatInterval) {
return IDPrivateTextAutoscrollStart(
autoscroll, target, scrollContainer, point, directions, repeatInterval);
}
NSDictionary<NSString *, id> *
IDPrivateTestingCopyUIKitAutoscrollSnapshot(id autoscroll) {
NSCParameterAssert(autoscroll != nil);
IDPrivateTestingRequireUIKitAutoscrollGetter(
autoscroll, @"target", @encode(id));
IDPrivateTestingRequireUIKitAutoscrollGetter(
autoscroll, TGPN_scrollContainer(), @encode(id));
IDPrivateTestingRequireUIKitAutoscrollGetter(
autoscroll, @"point", @encode(CGPoint));
IDPrivateTestingRequireUIKitAutoscrollGetter(
autoscroll, @"directions", @encode(int));
IDPrivateTestingRequireUIKitAutoscrollGetter(
autoscroll, @"repeatInterval", @encode(NSTimeInterval));
IDPrivateTestingRequireUIKitAutoscrollGetter(
autoscroll, @"count", @encode(NSUInteger));
IDPrivateTestingRequireUIKitAutoscrollGetter(
autoscroll, @"active", @encode(BOOL));
IDPrivateTestingRequireUIKitAutoscrollGetter(
autoscroll, @"disabled", @encode(BOOL));
id target = ((id (*)(id, SEL))objc_msgSend)(
autoscroll, NSSelectorFromString(@"target"));
id scrollContainer = ((id (*)(id, SEL))objc_msgSend)(
autoscroll, NSSelectorFromString(TGPN_scrollContainer()));
CGPoint point = ((CGPoint (*)(id, SEL))objc_msgSend)(
autoscroll, NSSelectorFromString(@"point"));
int directions = ((int (*)(id, SEL))objc_msgSend)(
autoscroll, NSSelectorFromString(@"directions"));
NSTimeInterval repeatInterval =
((NSTimeInterval (*)(id, SEL))objc_msgSend)(
autoscroll, NSSelectorFromString(@"repeatInterval"));
NSUInteger count = ((NSUInteger (*)(id, SEL))objc_msgSend)(
autoscroll, NSSelectorFromString(@"count"));
BOOL active = ((BOOL (*)(id, SEL))objc_msgSend)(
autoscroll, NSSelectorFromString(@"active"));
BOOL disabled = ((BOOL (*)(id, SEL))objc_msgSend)(
autoscroll, NSSelectorFromString(@"disabled"));
id timer = IDPrivateTestingUIKitAutoscrollTimer(autoscroll);
NSDate *timerFireDate =
[timer isKindOfClass:NSTimer.class] ? [(NSTimer *)timer fireDate] : nil;
return @{
@"active": @(active),
@"disabled": @(disabled),
@"targetPresent": @((BOOL)(target != nil)),
@"containerPresent": @((BOOL)(scrollContainer != nil)),
@"point": [NSValue valueWithCGPoint:point],
@"directions": @((NSUInteger)(unsigned int)directions),
@"repeatInterval": @(repeatInterval),
@"count": @(count),
@"timerIdentity":
@((unsigned long long)(uintptr_t)(__bridge void *)timer),
@"timerFireDate":
@(timerFireDate == nil
? 0.0
: timerFireDate.timeIntervalSinceReferenceDate),
};
}
void IDPrivateTestingSetUIKitAutoscrollDisabled(
id autoscroll,
BOOL disabled) {
NSString *selectorName = @"setDisabled:";
SEL selector = NSSelectorFromString(selectorName);
IDInteractionABISpec *spec = IDInteractionABISpecMake(
selectorName,
@encode(void),
@[IDInteractionTypeEncoding(@encode(BOOL))]);
NSString *mismatch = IDInteractionABIMismatchForSignature(
[autoscroll methodSignatureForSelector:selector],
spec);
if (mismatch != nil) {
@throw [NSException
exceptionWithName:NSInternalInconsistencyException
reason:[NSString stringWithFormat:
@"UIKit autoscroll oracle ABI mismatch: %@", mismatch]
userInfo:nil];
}
((void (*)(id, SEL, BOOL))objc_msgSend)(
autoscroll, selector, disabled);
}
void IDPrivateTestingFireUIKitAutoscroll(id autoscroll) {
NSString *selectorName = @"timerFired:";
SEL selector = NSSelectorFromString(selectorName);
IDInteractionABISpec *spec = IDInteractionABISpecMake(
selectorName,
@encode(void),
@[IDInteractionTypeEncoding(@encode(id))]);
NSString *mismatch = IDInteractionABIMismatchForSignature(
[autoscroll methodSignatureForSelector:selector],
spec);
if (mismatch != nil) {
@throw [NSException
exceptionWithName:NSInternalInconsistencyException
reason:[NSString stringWithFormat:
@"UIKit autoscroll oracle ABI mismatch: %@", mismatch]
userInfo:nil];
}
((void (*)(id, SEL, id))objc_msgSend)(
autoscroll, selector, nil);
}
void IDPrivateTestingInvalidateUIKitAutoscroll(id autoscroll) {
IDPrivateTextAutoscrollInvalidate(autoscroll);
}
void IDPrivateKeyboardInputChangedSelection(id delegate, id input) {
SEL selector = NSSelectorFromString(TGPN_keyboardInputChangedSelection_());
if ([delegate respondsToSelector:selector]) {
((void (*)(id, SEL, id))objc_msgSend)(delegate, selector, input);
}
}
NSUndoManager *IDPrivateInputUndoManager(id controller) {
return ((id (*)(id, SEL))objc_msgSend)(
controller,
NSSelectorFromString(@"undoManager"));
}
void IDPrivateInputCut(id controller, id sender) {
((void (*)(id, SEL, id))objc_msgSend)(controller, NSSelectorFromString(@"cut:"), sender);
}
void IDPrivateInputCopy(id controller, id sender) {
((void (*)(id, SEL, id))objc_msgSend)(controller, NSSelectorFromString(@"copy:"), sender);
}
void IDPrivateInputPaste(id controller, id sender) {
((void (*)(id, SEL, id))objc_msgSend)(controller, NSSelectorFromString(@"paste:"), sender);
}
void IDPrivateInputDetachFromLayoutManager(id controller) {
((void (*)(id, SEL))objc_msgSend)(
controller,
NSSelectorFromString(TGPN__detachFromLayoutManager()));
}
BOOL IDPrivateInputShouldHandlePaste(id controller, id sender) {
SEL action = @selector(paste:);
BOOL canHandle = ((BOOL (*)(id, SEL, SEL))objc_msgSend)(
controller,
NSSelectorFromString(TGPN__canHandleResponderAction_()),
action);
if (!canHandle) {
return NO;
}
return ((BOOL (*)(id, SEL, SEL, id))objc_msgSend)(
controller,
NSSelectorFromString(TGPN__shouldHandleResponderAction_withSender_()),
action,
sender);
}
UITextStorageDirection IDPrivateInputSelectionAffinity(id controller) {
return ((UITextStorageDirection (*)(id, SEL))objc_msgSend)(
controller,
NSSelectorFromString(@"selectionAffinity"));
}
NSDictionary<NSAttributedStringKey, id> *
IDPrivateInputTypingAttributes(id controller) {
SEL selector = NSSelectorFromString(@"typingAttributes");
IDPrivateRequireSignature(controller, selector, @encode(id), 2, NULL);
return ((id (*)(id, SEL))objc_msgSend)(controller, selector);
}
void IDPrivateInputSetTypingAttributes(
id controller,
NSDictionary<NSAttributedStringKey, id> *attributes) {
SEL selector = NSSelectorFromString(@"setTypingAttributes:");
IDPrivateRequireSignature(controller, selector, @encode(void), 3,
@encode(id));
((void (*)(id, SEL, id))objc_msgSend)(controller, selector, attributes);
}
void IDPrivateInputAllowTypingAttributeNames(
id controller,
NSSet<NSAttributedStringKey> *attributeNames) {
SEL getSelector = NSSelectorFromString(TGPN__allowedTypingAttributes());
SEL setSelector = NSSelectorFromString(TGPN__setAllowedTypingAttributes_());
IDPrivateRequireSignature(controller, getSelector, @encode(id), 2, NULL);
IDPrivateRequireSignature(controller, setSelector, @encode(void), 3,
@encode(id));
NSSet<NSAttributedStringKey> *current =
((id (*)(id, SEL))objc_msgSend)(controller, getSelector);
NSMutableSet<NSAttributedStringKey> *allowed =
current.mutableCopy ?: [NSMutableSet set];
[allowed unionSet:attributeNames];
((void (*)(id, SEL, id))objc_msgSend)(controller, setSelector,
allowed.copy);
}
void IDPrivateInputInvalidateTypingAttributes(id controller) {
SEL selector = NSSelectorFromString(TGPN__invalidateTypingAttributes());
IDPrivateRequireSignature(controller, selector, @encode(void), 2, NULL);
((void (*)(id, SEL))objc_msgSend)(controller, selector);
}
void IDPrivateInputPerformWhileSuppressingDelegateNotifications(
id controller, void (NS_NOESCAPE ^body)(void)) {
SEL selector = NSSelectorFromString(TGPN__performWhileSuppressingDelegateNotifications_());
[IDPrivateUIKit requireObject:controller
respondsToSelector:selector
stage:@"Suppressed input-delegate notification transaction"];
IDPrivateRequireSignature(controller, selector, @encode(void), 3,
@encode(void (^)(void)));
((void (*)(id, SEL, void (^)(void)))objc_msgSend)(controller, selector, body);
}
static id IDPrivateValidatedBackwardDeleteRanges(id controller, NSString *stage) {
id ranges = ((id (*)(id, SEL))objc_msgSend)(
controller,
NSSelectorFromString(TGPN__rangesForBackwardsDelete()));
[IDPrivateUIKit requireObject:ranges
respondsToSelector:NSSelectorFromString(TGPN_unionRange())
stage:stage];
return ranges;
}
void IDPrivateInputPreflightBackwardDeleteRanges(id controller) {
(void)IDPrivateValidatedBackwardDeleteRanges(
controller,
@"Backward-delete ranges construction preflight");
}
NSRange IDPrivateInputBackwardDeleteRange(id controller) {
id ranges = IDPrivateValidatedBackwardDeleteRanges(
controller,
@"Backward-delete ranges action aggregate");
return ((NSRange (*)(id, SEL))objc_msgSend)(
ranges,
NSSelectorFromString(TGPN_unionRange()));
}
BOOL IDPrivateTextViewShouldDelete(id textView, id input) {
return ((BOOL (*)(id, SEL, id))objc_msgSend)(
textView,
NSSelectorFromString(TGPN_keyboardInputShouldDelete_()),
input);
}
NSUInteger IDPrivateTextKit2RangeCount(id range) {
[IDPrivateUIKit requireObject:range
respondsToSelector:NSSelectorFromString(TGPN_textKit2Ranges())
stage:@"TextKit 2 range adapter"];
NSArray *ranges = ((id (*)(id, SEL))objc_msgSend)(
range,
NSSelectorFromString(TGPN_textKit2Ranges()));
return ranges.count;
}
NSArray *IDPrivateLayoutSelectionRects(id layoutController, id range, UIView *fromView) {
BOOL (^allContainers)(id) = ^BOOL(id container) { return YES; };
return ((id (*)(id, SEL, id, id, id))objc_msgSend)(
layoutController,
NSSelectorFromString(TGPN_selectionRectsForRange_fromView_forContainerPassingTest_()),
range,
fromView,
allContainers);
}
void IDPrivateTextContainerSetTextView(NSTextContainer *textContainer,
UIView * _Nullable textView) {
((void (*)(id, SEL, id))objc_msgSend)(
textContainer,
NSSelectorFromString(@"setTextView:"),
textView);
}
UIView *IDPrivateTextContainerTextView(NSTextContainer *textContainer) {
return ((id (*)(id, SEL))objc_msgSend)(
textContainer,
NSSelectorFromString(@"textView"));
}
void IDPrivateRequireTextContainerHost(id host, NSString *stage) {
NSArray<IDInteractionABISpec *> *specs = @[
IDInteractionABISpecMake(@"textContainer", @encode(id), @[]),
IDInteractionABISpecMake(@"setTextContainer:", @encode(void),
@[IDInteractionTypeEncoding(@encode(id))]),
IDInteractionABISpecMake(TGPN_textContainerOrigin(), @encode(CGPoint), @[]),
];
NSArray<NSString *> *mismatches = IDInteractionABIMismatches(host, specs);
if (mismatches.count == 0) return;
NSString *reason = [NSString stringWithFormat:
@"%@ failed: text-container host ABI mismatch:\n%@",
stage,
[mismatches componentsJoinedByString:@"\n"]];
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:reason
userInfo:nil];
}
BOOL IDPrivateRespondsToSelector(id target, SEL selector) {
return [target respondsToSelector:selector];
}
id IDPrivateObject0(id target, SEL selector) {
return ((id (*)(id, SEL))objc_msgSend)(target, selector);
}
id IDPrivateObject1(id target, SEL selector, id value) {
return ((id (*)(id, SEL, id))objc_msgSend)(target, selector, value);
}
id IDPrivateObject2(id target, SEL selector, id first, id second) {
return ((id (*)(id, SEL, id, id))objc_msgSend)(target, selector, first, second);
}
id IDPrivateObjectObjectInteger(id target, SEL selector, id value, NSInteger integer) {
return ((id (*)(id, SEL, id, NSInteger))objc_msgSend)(target, selector, value, integer);
}
id IDPrivateObjectObjectIntegerInteger(id target,
SEL selector,
id value,
NSInteger firstInteger,
NSInteger secondInteger) {
return ((id (*)(id, SEL, id, NSInteger, NSInteger))objc_msgSend)(
target,
selector,
value,
firstInteger,
secondInteger);
}
id IDPrivateObjectPoint(id target, SEL selector, CGPoint point) {
return ((id (*)(id, SEL, CGPoint))objc_msgSend)(target, selector, point);
}
id IDPrivateObjectPointObject(id target, SEL selector, CGPoint point, id value) {
return ((id (*)(id, SEL, CGPoint, id))objc_msgSend)(target, selector, point, value);
}
NSInteger IDPrivateInteger2(id target, SEL selector, id first, id second) {
return ((NSInteger (*)(id, SEL, id, id))objc_msgSend)(target, selector, first, second);
}
NSInteger IDPrivateIntegerObjectInteger(id target, SEL selector, id value, NSInteger integer) {
return ((NSInteger (*)(id, SEL, id, NSInteger))objc_msgSend)(target, selector, value, integer);
}
CGRect IDPrivateRectObject(id target, SEL selector, id value) {
return ((CGRect (*)(id, SEL, id))objc_msgSend)(target, selector, value);
}
void IDPrivateVoid0(id target, SEL selector) {
((void (*)(id, SEL))objc_msgSend)(target, selector);
}
void IDPrivateVoid1(id target, SEL selector, id value) {
((void (*)(id, SEL, id))objc_msgSend)(target, selector, value);
}
void IDPrivateVoid2(id target, SEL selector, id first, id second) {
((void (*)(id, SEL, id, id))objc_msgSend)(target, selector, first, second);
}
void IDPrivateVoidObjectRange(id target, SEL selector, id value, NSRange range) {
NSMethodSignature *signature = [target methodSignatureForSelector:selector];
BOOL valid = signature != nil &&
signature.numberOfArguments == 4 &&
strcmp(signature.methodReturnType, @encode(void)) == 0 &&
IDInteractionTypeMatches(
[signature getArgumentTypeAtIndex:2], @encode(id)) &&
IDPrivateTypeEncodingsMatch(
[signature getArgumentTypeAtIndex:3], @encode(NSRange));
if (!valid) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:[NSString stringWithFormat:
@"Private operation ABI is unavailable for %@",
NSStringFromSelector(selector)]
userInfo:nil];
}
((void (*)(id, SEL, id, NSRange))objc_msgSend)(target, selector, value, range);
}
void IDPrivateVoidIntegerObject(id target, SEL selector, NSInteger integer, id value) {
((void (*)(id, SEL, NSInteger, id))objc_msgSend)(target, selector, integer, value);
}
#endif