-
-
Save paradiseduo/592a589c4eaf372e2558155bc64b0d20 to your computer and use it in GitHub Desktop.
iOS 14剪切板的ProbableWebURL逻辑
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// PBProbableWebDataDetective.m | |
// MyPasteboard | |
// | |
// Created by xxx on 2021/7/21. | |
// | |
#import "PBProbableWebDataDetective.h" | |
#import <CFNetwork/CFNetwork.h> | |
//#import <UIKit/UIKit.h> | |
static NSString *const _UIPasteboardDetectionPatternProbableWebURL = @"com.apple.uikit.pasteboard-detection-pattern.probable-web-url"; | |
static NSString *const _UIPasteboardDetectionPatternProbableWebSearch = @"com.apple.uikit.pasteboard-detection-pattern.probable-web-search"; | |
static NSString *const _UIPasteboardDetectionPatternNumber = @"com.apple.uikit.pasteboard-detection-pattern.number"; | |
int sub_100011D1C(NSString *value); | |
extern "C" { | |
extern BOOL _CFHostIsDomainTopLevel(NSString *host); // 判断是否顶级域名,如.com、.ac.uk | |
} | |
@interface NSString (WebNSURLExtras) | |
-(BOOL)_webkit_looksLikeAbsoluteURL; // 可以参考下面的实现,demo直接用Foundation的私有接口 | |
-(BOOL)_web_looksLikeIPAddress; // 先不翻译,demo直接用Foundation的私有接口 | |
@end | |
@implementation NSString (WebNSURLExtras) | |
-(NSString *)_webkit_stringByTrimmingWhitespace2 | |
{ | |
NSMutableString *trimmed = [self mutableCopy]; | |
CFStringTrimWhitespace((__bridge CFMutableStringRef)trimmed); | |
return trimmed; | |
} | |
-(NSRange)_webkit_rangeOfURLScheme2 | |
{ | |
NSRange colon = [self rangeOfString:@":"]; | |
if (colon.location != NSNotFound && colon.location > 0) { | |
NSRange scheme = {0, colon.location}; | |
static NSCharacterSet *InverseSchemeCharacterSet = nil; | |
if (!InverseSchemeCharacterSet) { | |
NSString *acceptableCharacters = @"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+.-"; | |
InverseSchemeCharacterSet = [[NSCharacterSet characterSetWithCharactersInString:acceptableCharacters] invertedSet]; | |
} | |
NSRange illegals = [self rangeOfCharacterFromSet:InverseSchemeCharacterSet options:0 range:scheme]; | |
if (illegals.location == NSNotFound) | |
return scheme; | |
} | |
return NSMakeRange(NSNotFound, 0); | |
} | |
-(BOOL)_webkit_looksLikeAbsoluteURL2 | |
{ | |
// Trim whitespace because _web_URLWithString allows whitespace. | |
return [[self _webkit_stringByTrimmingWhitespace2] _webkit_rangeOfURLScheme2].location != NSNotFound; | |
} | |
- (BOOL)_web_looksLikeIPAddress2 | |
{ | |
return YES; | |
} | |
@end | |
@interface NSString (safari) | |
- (NSString *)safari_stringByRemovingUnnecessaryCharactersFromUserTypedURLString; | |
- (NSString *)safari_possibleTopLevelDomainCorrectionForUserTypedString; | |
- (NSString *)safari_highLevelDomainFromHost; | |
- (NSString *)safari_topLevelDomainUsingCFFromComponents:(NSArray *)comp; | |
- (void)safari_reverseEnumerateComponents:(NSArray *)array usingBlock:(void (^)(NSString *,int,int*))block; | |
@end | |
@implementation NSString(safari) | |
- (NSString *)safari_stringByRemovingUnnecessaryCharactersFromUserTypedURLString | |
{ | |
NSMutableString *v6 = [[self stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] mutableCopy]; | |
[v6 replaceOccurrencesOfString:@"\n" withString:@"" options:0 range:NSMakeRange(0, [v6 length])]; | |
[v6 replaceOccurrencesOfString:@"\r" withString:@"" options:0 range:NSMakeRange(0, [v6 length])]; | |
if ([v6 length] && [v6 characterAtIndex:0] == '<') | |
{ | |
[v6 deleteCharactersInRange:NSMakeRange(0, 1)]; | |
NSUInteger v7 = [@"URL:" length]; | |
if ([v6 length] >= v7 && ![v6 compare:@"URL:" options:1 range:NSMakeRange(0, v7)]) { | |
[v6 deleteCharactersInRange:NSMakeRange(0, v7)]; | |
} | |
NSUInteger v9 = [v6 length]; | |
if (v9) { | |
if ([v6 characterAtIndex:v9-1] == '>') { | |
[v6 deleteCharactersInRange:NSMakeRange(v9-1, 1)]; | |
} | |
} | |
} | |
return v6; | |
} | |
- (NSString *)safari_possibleTopLevelDomainCorrectionForUserTypedString | |
{ | |
static NSDictionary *qword_100022998 = nil; | |
if (!qword_100022998) { | |
qword_100022998 = [[NSDictionary alloc] initWithObjectsAndKeys:@".com",@".cmo",@".com",@"c.om", | |
@".com",@".xom",@".net",@".ent", | |
@".net",@".ent",@".net",@".nte", | |
@".org",@".ogr",@".org",@".rog", | |
@".info",@".ifno",@".info",@".ifnp",nil]; | |
} | |
NSUInteger v23 = [self length]; | |
BOOL v3 = [self hasSuffix:@"."]; | |
v23 -= v3; | |
for (NSString *key in qword_100022998) { | |
NSRange v9 = [self rangeOfString:key options:13 range:NSMakeRange(0, v23)]; | |
if (v9.location != NSNotFound) { | |
return [self stringByReplacingCharactersInRange:v9 withString:key]; | |
} | |
} | |
return nil; | |
} | |
- (NSString *)safari_highLevelDomainFromHost | |
{ | |
if (![self length]) return nil; | |
if ([self _web_looksLikeIPAddress]) { | |
return self; | |
} | |
NSArray *array = [self componentsSeparatedByString:@"."]; | |
return [self safari_topLevelDomainUsingCFFromComponents:array]; | |
} | |
- (NSString *)safari_topLevelDomainUsingCFFromComponents:(NSArray *)comp | |
{ | |
__block NSString *topLevelDomain = nil; | |
__block BOOL isTopLevelDomain = NO; | |
[self safari_reverseEnumerateComponents:comp usingBlock:^(NSString *host, int index, int *stop) { | |
BOOL isHost = _CFHostIsDomainTopLevel(host); | |
topLevelDomain = host; | |
if (isHost == YES) { | |
isTopLevelDomain = YES; | |
} | |
else { | |
if (isTopLevelDomain == YES) { | |
*stop = YES; | |
} | |
} | |
}]; | |
if (isTopLevelDomain) { | |
if (self.length != topLevelDomain.length) { | |
return topLevelDomain; | |
} | |
return self; | |
} | |
return nil; | |
} | |
- (void)safari_reverseEnumerateComponents:(NSArray *)array usingBlock:(void (^)(NSString *,int,int*))block | |
{ | |
NSEnumerator *enumerator = [array reverseObjectEnumerator]; | |
NSString *object; | |
NSMutableString *mString; | |
int idx = 0; | |
int res = 0; | |
while (object = [enumerator nextObject]) { | |
if (mString == nil) { | |
mString = [NSMutableString stringWithCapacity:16]; | |
} | |
else { | |
[mString insertString:@"." atIndex:0]; | |
} | |
[mString insertString:object atIndex:0]; | |
res = 0; | |
block(mString, idx, &res); | |
if (res) break;; | |
idx++; | |
} | |
return; | |
} | |
@end | |
// 返回":"的range | |
NSRange sub_100012CEE(NSString *value) | |
{ | |
if ([value length]) { | |
return [value rangeOfString:@":"]; | |
} | |
return NSMakeRange(NSNotFound, -1); | |
} | |
// 有":"返回":"前的字符串,没有返回空 | |
NSString *sub_100012934(NSString *value) | |
{ | |
NSRange range = sub_100012CEE(value); | |
if (range.location == NSNotFound) { | |
return nil; | |
} | |
return [value substringToIndex:range.location]; | |
} | |
int sub_1000129A8(NSString *value, int* arg2) | |
{ | |
int res = 0; | |
if (![value length]) return 0; | |
NSString *v4 = [value lowercaseString]; | |
NSString *v6 = [v4 safari_possibleTopLevelDomainCorrectionForUserTypedString]; | |
if ([v6 length]) { | |
v4 = v6; | |
} | |
NSString *v8 = [v4 safari_highLevelDomainFromHost]; | |
if ([v8 length]) { | |
*arg2 = 2 *([v4 rangeOfString:@"@"].location == NSNotFound); | |
return 1; | |
} | |
NSUInteger length = [v4 length]; | |
static NSSet *v19 = nil; | |
if (!v19) { | |
v19 = [[NSSet alloc] initWithObjects:@"private", @"box", @"local", nil]; | |
} | |
NSUInteger index = [v4 rangeOfString:@"." options:NSBackwardsSearch].location; | |
if (!length || index == NSNotFound || (index+1) == length) { | |
return 0; | |
} | |
NSString *v17 = [v4 substringFromIndex:index+1]; | |
if ([v19 containsObject:[v17 lowercaseString]]) { | |
*arg2 = 2 *([v4 rangeOfString:@"@"].location == NSNotFound); | |
return 1; | |
} | |
return res; | |
} | |
// | |
NSCharacterSet *sub_100012D62() | |
{ | |
NSMutableCharacterSet *res = [[NSMutableCharacterSet alloc] init]; | |
[res formUnionWithCharacterSet:[NSCharacterSet decimalDigitCharacterSet]]; | |
[res formUnionWithCharacterSet:[NSCharacterSet punctuationCharacterSet]]; | |
[res formUnionWithCharacterSet:[NSCharacterSet symbolCharacterSet]]; | |
return [res invertedSet]; | |
} | |
// 判断核心函数,<3说明匹配成功 | |
int sub_100011D1C(NSString *value) | |
{ | |
// XXX: 有tls缓存的逻辑,这里简化掉了 | |
int res = 3; | |
value = [value copy]; | |
NSCharacterSet *whiteCharacterSet = [NSCharacterSet whitespaceAndNewlineCharacterSet]; | |
NSString *valueAfterTtrim = [value stringByTrimmingCharactersInSet:whiteCharacterSet]; | |
NSString *valueAfterSafari = nil; | |
if (![valueAfterTtrim length]) return 4; | |
valueAfterSafari = [valueAfterTtrim safari_stringByRemovingUnnecessaryCharactersFromUserTypedURLString]; | |
if (![valueAfterSafari length]) return 0; | |
NSRange rangeWC = [valueAfterSafari rangeOfCharacterFromSet:whiteCharacterSet]; | |
NSRange rangeSlash = [valueAfterSafari rangeOfString:@"/"]; | |
if (rangeWC.location != NSNotFound ) { // 有空格的情况 | |
NSString *subString2WC = [valueAfterSafari substringToIndex:rangeWC.location]; // 空格前字符 | |
NSString *subString2Colon = sub_100012934(subString2WC); // 冒号前字符 | |
BOOL v24 = NO; | |
if (!subString2Colon.length) { | |
v24 = YES; | |
} | |
else { | |
static NSSet *qword_1000229C0 = nil; | |
if (!qword_1000229C0) { | |
qword_1000229C0 = [[NSSet alloc] initWithObjects:@"data",@"file",@"ftp",@"ftps",@"http",@"https",@"javascript",nil]; | |
} | |
v24 = [qword_1000229C0 containsObject:subString2Colon]; | |
} | |
if (!v24) return 0; | |
NSUInteger v25 = [subString2Colon length]; | |
if (!v25 || v25 != subString2WC.length-1) { | |
res = 1; | |
if ([subString2WC _webkit_looksLikeAbsoluteURL]) { | |
return res; | |
} | |
if ([subString2WC rangeOfString:@"?"].location == NSNotFound) { // 有空格,同时带问号的话,会走到下面 LABEL_36 共同的逻辑 | |
return 0; | |
} | |
} | |
else { | |
return 0; | |
} | |
} | |
// 找不到 '/' | |
if (rangeSlash.location == NSNotFound) { | |
NSString *v15 = valueAfterSafari; | |
NSRange v16 = [v15 rangeOfString:@":" options:NSBackwardsSearch]; | |
if (v16.location == NSNotFound) { | |
goto LABEL_36; | |
} | |
NSUInteger v17 = [v15 length] - 1; | |
if (v16.location != v17) { | |
NSUInteger v18 = v16.location + 1; | |
while (v18 < v17) { | |
unichar v19 = [v15 characterAtIndex:v18]; | |
if (v19 <= 0xFFu) { | |
++v18; | |
if ( (_DefaultRuneLocale.__runetype[v19] & 0x400) != 0 ) | |
continue; | |
} | |
valueAfterSafari = v15; | |
goto LABEL_36; | |
}; | |
valueAfterSafari = [v15 substringToIndex:v16.location]; | |
} | |
} | |
LABEL_36: | |
NSUInteger length = [valueAfterSafari length]; | |
BOOL go_LABEL_47 = NO; | |
if (length && ([valueAfterSafari characterAtIndex:length-1] == ':')) { | |
NSRange rangeOfColon = sub_100012CEE(valueAfterSafari); | |
if (rangeOfColon.location == (length-1)) { // 最后一个字符是: | |
go_LABEL_47 = YES; | |
} | |
} | |
if (!go_LABEL_47 && [valueAfterSafari _webkit_looksLikeAbsoluteURL]) { | |
NSString *valueBeforeColon = sub_100012934(valueAfterSafari); | |
if (rangeSlash.location == NSNotFound) { // XXX: 找不到'/' | |
res = 2; | |
if (![valueAfterSafari isEqualToString:@"about:blank"]) { | |
if ([valueBeforeColon length]) { | |
static NSSet *qword_1000229E0 = nil; | |
if (!qword_1000229E0) { | |
qword_1000229E0 = [[NSSet alloc] initWithObjects:@"data", @"facetime", @"gamecenter", @"irc", "javascript", "mailto", "man", "message", "radar", "spotify", "tel", nil]; | |
} | |
unsigned char v42 = [qword_1000229E0 containsObject:valueBeforeColon]; // XXX: 没转小写 | |
res = 2*(v42^1u) + 1; | |
} | |
else { | |
res = 3; | |
} | |
} | |
} | |
else { | |
static NSSet *qword_1000229D0 = nil; | |
if (!qword_1000229D0) { | |
qword_1000229D0 = [[NSSet alloc] initWithObjects:@"site", @"link", @"related", @"cache", nil]; | |
} | |
if (valueBeforeColon.length) { | |
unsigned char v36 = [qword_1000229D0 containsObject:[valueBeforeColon lowercaseString]]; | |
res = v36 ^ 1u; | |
} | |
else { | |
res = 1; | |
} | |
} | |
return res; // ok | |
} | |
//LABEL_47: | |
NSRange v37 = [valueAfterSafari rangeOfString:@"."]; | |
if (rangeSlash.location == NSNotFound) { | |
int v55; | |
if ( v37.location && v37.location != NSNotFound && sub_1000129A8(valueAfterSafari, &v55)) { | |
res = v55; | |
} | |
else { | |
res = 0x3 +(([valueAfterSafari caseInsensitiveCompare:@"localhost"]==0)?-1:0); | |
} | |
return res; | |
} | |
res = 3; | |
static NSCharacterSet *qword_1000229F0 = nil; | |
if (!qword_1000229F0) { | |
qword_1000229F0 = sub_100012D62(); | |
} | |
if ([valueAfterSafari rangeOfCharacterFromSet:qword_1000229F0].location == NSNotFound) { | |
return res; | |
} | |
int v55 = 0; | |
NSString *v40 = nil; | |
if (v37.location && v37.location != NSNotFound && (v40 = [valueAfterSafari substringToIndex:v37.location]) && sub_1000129A8(v40, &v55)) { | |
res = v55; | |
} | |
else { | |
res = 2; | |
rangeSlash = [valueAfterSafari rangeOfString:@"/" options:NSBackwardsSearch]; | |
if (rangeSlash.location != [valueAfterSafari length]-1) { | |
NSRange v44 = [valueAfterSafari rangeOfString:@"#" options:NSBackwardsSearch]; | |
if (v44.location==NSNotFound || v44.location < rangeSlash.location) { | |
NSRange v45 = [valueAfterSafari rangeOfString:@"?" options:NSBackwardsSearch]; | |
if (v45.location==NSNotFound || v45.location <= rangeSlash.location) { | |
res = ((unsigned char)[valueAfterSafari hasPrefix:@"localhost/"]) ^ 3LL; | |
} | |
} | |
} | |
} | |
return res; | |
} | |
@implementation PBProbableWebDataDetective | |
- (id)detectedPatternValuesInValue:(NSString *)value | |
{ | |
// @"com.apple.uikit.pasteboard-detection-pattern.probable-web-url" | |
if (![value isKindOfClass:NSString.class]) return nil; | |
NSDictionary *res = nil; | |
NSCharacterSet *whitesCharacterSet = [NSCharacterSet whitespaceAndNewlineCharacterSet]; | |
NSArray *components = [value componentsSeparatedByCharactersInSet:whitesCharacterSet]; | |
for (NSString *comp in components) { | |
if (sub_100011D1C(comp) < 3) { | |
res = @{ | |
_UIPasteboardDetectionPatternProbableWebURL:comp | |
}; | |
return res; | |
} | |
} | |
int v16 = sub_100011D1C(value); | |
if (v16 && v16 < 3) { | |
res = @{ | |
_UIPasteboardDetectionPatternProbableWebURL:value | |
}; | |
return res; | |
} | |
if (v16==0 || v16==3) { | |
res = @{ | |
_UIPasteboardDetectionPatternProbableWebSearch:value | |
}; | |
return res; | |
} | |
return nil; | |
} | |
@end | |
@implementation PBNumberDataDetective | |
- (id)detectedPatternValuesInValue:(NSString *)value | |
{ | |
if (![value isKindOfClass:NSString.class]) return @{}; | |
NSLocale *v7 = [NSLocale currentLocale]; | |
NSString *v9 = [v7 groupingSeparator]; | |
NSString *v11 = value; | |
if (v9) { | |
v11 = [value stringByReplacingOccurrencesOfString:v9 withString:@""]; | |
} | |
if (![v11 length]) return @{}; | |
NSScanner *v13 = [NSScanner scannerWithString:v11]; | |
[v13 setLocale:v7]; | |
double res; | |
if ([v13 scanDouble:&res]) { | |
return @{_UIPasteboardDetectionPatternNumber: @(res)}; | |
} | |
return @{}; | |
} | |
@end | |
void TestSafari() | |
{ | |
NSLog(@"safari: %@", [@"ddd.333.xxx.com" safari_highLevelDomainFromHost]); | |
NSLog(@"safari: %@", [@"ddd.333.xxx.444" safari_highLevelDomainFromHost]); | |
} | |
void TestWebDetective() | |
{ | |
NSLog(@"--------------------------- web ----------------------"); | |
PBProbableWebDataDetective *detective = [PBProbableWebDataDetective new]; | |
NSArray *okCases = @[ | |
@"8.:/ Юt7hud", | |
@"8-:/Юt7hud", | |
@"8-:/Юt7hud", | |
@"3[得意]`:/ ⻓按复制此条消息,打开XXX,口令xxx", | |
@"http://kkk/?" | |
]; | |
NSArray *badCases = @[ | |
@"8~:/ Юt7hudZhJeLDWj8ΔΔ", | |
@"3 8`:/ ⻓按复制此条消息,打开XXX,口令xxx", | |
]; | |
for (NSString *value in okCases) { | |
NSDictionary *result = [detective detectedPatternValuesInValue:value]; | |
NSCAssert([result objectForKey:_UIPasteboardDetectionPatternProbableWebURL], @""); | |
NSLog(@"ok cases:%@", result); | |
} | |
for (NSString *value in badCases) { | |
NSDictionary *result = [detective detectedPatternValuesInValue:value]; | |
NSCAssert([result objectForKey:_UIPasteboardDetectionPatternProbableWebURL]==nil, @""); | |
NSLog(@"bad cases:%@", result); | |
} | |
} | |
void TestNumberDetective() | |
{ | |
NSLog(@"--------------------------- number ----------------------"); | |
PBNumberDataDetective *detective = [PBNumberDataDetective new]; | |
NSArray *okCases = @[ | |
@"8~:/ Юt7hud", | |
@"1 8`:/ ⻓按复制此条消息,打开XXX,口令xxx", | |
]; | |
for (NSString *value in okCases) { | |
NSDictionary *result = [detective detectedPatternValuesInValue:value]; | |
NSCAssert([result objectForKey:_UIPasteboardDetectionPatternNumber], @""); | |
NSLog(@"ok cases:%@", result); | |
}} | |
void TestDetective() | |
{ | |
TestWebDetective(); | |
TestNumberDetective(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment