2017-09-22 72 views
3

使用CharacterSet时遇到了一个有趣的问题。从我迄今为止收集到的CharacterSet是基于UnicodeScalar;你可以用标量初始化它并检查标量是否包含在集合中。查询该集合以确定它是否包含一个Character,其字形可能由几个unicode标量值组成,这没有意义。检查单个UnicodeScalar的CharacterSet会产生奇怪的行为

我的问题在于当我使用表情符号进行测试时,这是一个单一的Unicode标量值(十进制中的128518)。由于这是我会想到它会工作,并在这里单个Unicode标量值是结果:

"" == UnicodeScalar(128518)! // true 

// A few variations to show exactly what is being set up 
let supersetA = CharacterSet(charactersIn: "") 
let supersetB = CharacterSet(charactersIn: "A") 
let supersetC = CharacterSet(charactersIn: UnicodeScalar(128518)!...UnicodeScalar(128518)!) 
let supersetD = CharacterSet(charactersIn: UnicodeScalar(65)...UnicodeScalar(65)).union(CharacterSet(charactersIn: UnicodeScalar(128518)!...UnicodeScalar(128518)!)) 

supersetA.contains(UnicodeScalar(128518)!) // true 
supersetB.contains(UnicodeScalar(128518)!) // false 
supersetC.contains(UnicodeScalar(128518)!) // true 
supersetD.contains(UnicodeScalar(128518)!) // false 

正如你所看到的,检查工作,如果在CharacterSet包含一个标值(可能由于优化),但在任何其他情况下,它不会按预期工作。

我无法找到有关的CharacterSet下级执行,还是它的工作原理在一定的编码(即UTF-16一样NSString)的任何信息,但随着API涉及了很多与UnicodeScalar我很惊讶,它的失败是这样,我不确定它为什么会发生,或者如何进一步调查。

任何人都可以阐明为什么这可能是什么?

+0

似乎是基金会(或Swift标准库)的一些错误。 'supersetD'的情况在Xcode 9中返回'true',所以'union(_ :)'的错误似乎在最新的SDK中得到修复。解决方法:'CharacterSet(charactersIn:“”).union(CharacterSet(charactersIn:“A”))''。 – OOPer

+0

它变得更加怪异:https://pastebin.com/zCrM1XUL。我做了一些挖掘,你可能想看看'CFCharacterSet.c'中的'_CFCharacterSetIsLongCharacterMember',这就是contains方法的作用(我确信我不太了解它)。 https://github.com/apple/swift-corelibs-foundation/blob/d95964015bed377baa99c3612281afa11bf06990/CoreFoundation/String.subproj/CFCharacterSet.c#L1716 – nyg

+0

@nyg我花了大把的时间搞清楚它是什么,所以如果你我很好奇,看看下面的答案。 –

回答

7

源代码CharacterSetis available, actually.的来源contains是:

fileprivate func contains(_ member: Unicode.Scalar) -> Bool { 
    switch _backing { 
    case .immutable(let cs): 
     return CFCharacterSetIsLongCharacterMember(cs, member.value) 
    case .mutable(let cs): 
     return CFCharacterSetIsLongCharacterMember(cs, member.value) 
    } 
} 

因此,它基本上只是调用,直至CFCharacterSetIsLongCharacterMember。该is also available, although only for Yosemite的源代码(El Cap和Sierra的版本都表示“即将推出”)。然而,优胜美地的代码似乎与我在Sierra上的反汇编中看到的相符。无论如何,代码如下所示:

Boolean CFCharacterSetIsLongCharacterMember(CFCharacterSetRef theSet, UTF32Char theChar) { 
    CFIndex length; 
    UInt32 plane = (theChar >> 16); 
    Boolean isAnnexInverted = false; 
    Boolean isInverted; 
    Boolean result = false; 

    CF_OBJC_FUNCDISPATCHV(__kCFCharacterSetTypeID, Boolean, (NSCharacterSet *)theSet, longCharacterIsMember:(UTF32Char)theChar); 

    __CFGenericValidateType(theSet, __kCFCharacterSetTypeID); 

    if (plane) { 
     CFCharacterSetRef annexPlane; 

     if (__CFCSetIsBuiltin(theSet)) { 
      isInverted = __CFCSetIsInverted(theSet); 
      return (CFUniCharIsMemberOf(theChar, __CFCSetBuiltinType(theSet)) ? !isInverted : isInverted); 
     } 

     isAnnexInverted = __CFCSetAnnexIsInverted(theSet); 

     if ((annexPlane = __CFCSetGetAnnexPlaneCharacterSetNoAlloc(theSet, plane)) == NULL) { 
      if (!__CFCSetHasNonBMPPlane(theSet) && __CFCSetIsRange(theSet)) { 
       isInverted = __CFCSetIsInverted(theSet); 
       length = __CFCSetRangeLength(theSet); 
       return (length && __CFCSetRangeFirstChar(theSet) <= theChar && theChar < __CFCSetRangeFirstChar(theSet) + length ? !isInverted : isInverted); 
      } else { 
       return (isAnnexInverted ? true : false); 
      } 
     } else { 
      theSet = annexPlane; 
      theChar &= 0xFFFF; 
     } 
    } 

    isInverted = __CFCSetIsInverted(theSet); 

    switch (__CFCSetClassType(theSet)) { 
     case __kCFCharSetClassBuiltin: 
      result = (CFUniCharIsMemberOf(theChar, __CFCSetBuiltinType(theSet)) ? !isInverted : isInverted); 
      break; 

     case __kCFCharSetClassRange: 
      length = __CFCSetRangeLength(theSet); 
      result = (length && __CFCSetRangeFirstChar(theSet) <= theChar && theChar < __CFCSetRangeFirstChar(theSet) + length ? !isInverted : isInverted); 
      break; 

     case __kCFCharSetClassString: 
      result = ((length = __CFCSetStringLength(theSet)) ? (__CFCSetBsearchUniChar(__CFCSetStringBuffer(theSet), length, theChar) ? !isInverted : isInverted) : isInverted); 
      break; 

     case __kCFCharSetClassBitmap: 
      result = (__CFCSetCompactBitmapBits(theSet) ? (__CFCSetIsMemberBitmap(__CFCSetBitmapBits(theSet), theChar) ? true : false) : isInverted); 
      break; 

     case __kCFCharSetClassCompactBitmap: 
      result = (__CFCSetCompactBitmapBits(theSet) ? (__CFCSetIsMemberInCompactBitmap(__CFCSetCompactBitmapBits(theSet), theChar) ? true : false) : isInverted); 
      break; 

     default: 
      CFAssert1(0, __kCFLogAssertion, "%s: Internal inconsistency error: unknown character set type", __PRETTY_FUNCTION__); // We should never come here 
      return false; // To make compiler happy 
    } 

    return (result ? !isAnnexInverted : isAnnexInverted); 
} 

因此,我们可以跟进,并找出发生了什么事。不幸的是,我们必须破解我们的x86_64组装技能才能做到这一点。但是不要害怕,因为我已经为你做了这件事,因为显然这是我在周五晚上为了好玩而做的。

一个有用的事情已经是数据结构:

struct __CFCharacterSet { 
    CFRuntimeBase _base; 
    CFHashCode _hashValue; 
    union { 
     struct { 
      CFIndex _type; 
     } _builtin; 
     struct { 
      UInt32 _firstChar; 
      CFIndex _length; 
     } _range; 
     struct { 
      UniChar *_buffer; 
      CFIndex _length; 
     } _string; 
     struct { 
      uint8_t *_bits; 
     } _bitmap; 
     struct { 
      uint8_t *_cBits; 
     } _compactBitmap; 
    } _variants; 
    CFCharSetAnnexStruct *_annex; 
}; 

我们需要知道到底什么CFRuntimeBase是,太:

typedef struct __CFRuntimeBase { 
    uintptr_t _cfisa; 
    uint8_t _cfinfo[4]; 
#if __LP64__ 
    uint32_t _rc; 
#endif 
} CFRuntimeBase; 

你猜怎么着!还有一些我们需要的常量。

enum { 
     __kCFCharSetClassTypeMask = 0x0070, 
      __kCFCharSetClassBuiltin = 0x0000, 
      __kCFCharSetClassRange = 0x0010, 
      __kCFCharSetClassString = 0x0020, 
      __kCFCharSetClassBitmap = 0x0030, 
      __kCFCharSetClassSet = 0x0040, 
      __kCFCharSetClassCompactBitmap = 0x0040, 
    // irrelevant stuff redacted 
}; 

然后我们就可以在CFCharacterSetIsLongCharacterMember突破和日志结构:

supersetA.contains(UnicodeScalar(128518)!) 

(lldb) po [NSData dataWithBytes:$rdi length:48] 
<21b3d2ad ffff1d00 90190000 02000000 00000000 00000000 06f60100 00000000 01000000 00000000 00000000 00000000> 

基于上面的结构,我们可以计算出这是什么字符集而成的。在这种情况下,相关部分将是从CFRuntimeBase开始的cfinfo的第一个字节,它们是字节9-12。这个的第一个字节0x90包含字符集的类型信息。它需要AND编辑与__kCFCharSetClassTypeMask,这得到我们0x10,这是__kCFCharSetClassRange

对于此行:

supersetB.contains(UnicodeScalar(128518)!) 

的结构是:

(lldb) po [NSData dataWithBytes:$rdi length:48] 
<21b3d2ad ffff1d00 a0190000 02000000 00000000 00000000 9066f000 01000000 02000000 00000000 00000000 00000000> 

这次字节9是0xa0,其AND ED与掩模是0x20__kCFCharSetClassString

此时,Monty Python演员阵容尖叫着“Get On With It!”,所以让我们来看看CFCharacterSetIsLongCharacterMember的来源,看看发生了什么。

跳过过去所有的CF_OBJC_FUNCDISPATCHV废话,我们得到这一行:

if (plane) { 

这显然计算结果为真在这两种情况下。下一个测试:

if (__CFCSetIsBuiltin(theSet)) { 

此计算为在这两种情况下错误的,因为无论是类型__kCFCharSetClassBuiltin,所以我们跳过这个块。

isAnnexInverted = __CFCSetAnnexIsInverted(theSet); 

在这两种情况下,_annex指针为空(见在结构的端部的所有零),所以这是false

本次测试将是true出于同样的原因:

if ((annexPlane = __CFCSetGetAnnexPlaneCharacterSetNoAlloc(theSet, plane)) == NULL) { 

带着我们:

if (!__CFCSetHasNonBMPPlane(theSet) && __CFCSetIsRange(theSet)) { 

__CFCSetHasNonBMPPlane宏检查_annex,所以这是假的。当然,表情符号不在BMP平面中,所以这实际上对于两个个案都是错误的,即使是返回正确结果的情况也是如此。

__CFCSetIsRange检查我们的类型是否为__kCFCharSetClassRange,这是第一次。所以这是我们的分歧点。这样做的第二次调用,产生不正确的结果,返回到下一行:

return (isAnnexInverted ? true : false); 

而且由于附件是NULL,造成isAnnexInverted是假的,这个返回false。

至于如何解决它......好吧,我不能。但现在我们知道它为什么发生了。据我所知,主要的问题是在创建字符集时_annex字段未被填充,并且由于附件似乎用于跟踪非BMP平面中的字符,我认为它应该对于两个字符集都存在。顺便说一下,如果您决定使用file one(我会将其归档于CoreFoundation,因为这是实际问题所在),此信息可能对您的错误报告有所帮助。

+0

你我的朋友...是一个传奇!你深入那一个!我会得到错误报告! –