2017-09-22 49 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

你我的朋友...是一個傳奇!你深入那一個!我會得到錯誤報告! –