检查单个 Unicode 标量的字符集会产生奇怪的行为
Checking CharacterSet for single UnicodeScalar yields strange behaviour
在使用 CharacterSet
时,我遇到了一个有趣的问题。据我目前收集到的信息,CharacterSet
是基于 UnicodeScalar
;您可以使用标量对其进行初始化,并检查集合中是否包含标量。查询集合以查明它是否包含 Character
,谁的字形可能由几个 unicode 标量值组成,没有意义。
我的问题出在测试表情符号时,表情符号是单个 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
的低级实现的信息,也找不到它是否适用于某种编码(即像 NSString
这样的 UTF-16),但作为 API处理了很多 UnicodeScalar
我很惊讶它会这样失败,我不确定为什么会这样,或者如何进一步调查。
任何人都可以解释为什么会这样吗?
CharacterSet
的源代码 is 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 的版本都说 "Coming Soon")。然而,Yosemite 代码似乎与我在 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
包含字符集的类型信息。它需要用 __kCFCharSetClassTypeMask
编辑 AND
,这让我们得到 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
加上掩码是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
,这仅在第一次为真。所以这就是我们的分歧点。第二次调用它,产生不正确的结果,returns 在下一行:
return (isAnnexInverted ? true : false);
而且由于附件是NULL
,导致isAnnexInverted
是假的,这个returns是假的。
至于如何修复……好吧,我不会。但现在我们知道为什么会这样了。据我所知,主要问题是创建字符集时 _annex
字段未被填充,并且由于附件似乎用于跟踪非 BMP 平面中的字符,我认为这两个字符集都应该存在。顺便说一句,如果您决定 file one,此信息可能会对错误报告有所帮助(我会针对 CoreFoundation 提交它,因为那是实际问题所在)。
在使用 CharacterSet
时,我遇到了一个有趣的问题。据我目前收集到的信息,CharacterSet
是基于 UnicodeScalar
;您可以使用标量对其进行初始化,并检查集合中是否包含标量。查询集合以查明它是否包含 Character
,谁的字形可能由几个 unicode 标量值组成,没有意义。
我的问题出在测试表情符号时,表情符号是单个 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
的低级实现的信息,也找不到它是否适用于某种编码(即像 NSString
这样的 UTF-16),但作为 API处理了很多 UnicodeScalar
我很惊讶它会这样失败,我不确定为什么会这样,或者如何进一步调查。
任何人都可以解释为什么会这样吗?
CharacterSet
的源代码 is 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 的版本都说 "Coming Soon")。然而,Yosemite 代码似乎与我在 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
包含字符集的类型信息。它需要用 __kCFCharSetClassTypeMask
编辑 AND
,这让我们得到 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
加上掩码是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
,这仅在第一次为真。所以这就是我们的分歧点。第二次调用它,产生不正确的结果,returns 在下一行:
return (isAnnexInverted ? true : false);
而且由于附件是NULL
,导致isAnnexInverted
是假的,这个returns是假的。
至于如何修复……好吧,我不会。但现在我们知道为什么会这样了。据我所知,主要问题是创建字符集时 _annex
字段未被填充,并且由于附件似乎用于跟踪非 BMP 平面中的字符,我认为这两个字符集都应该存在。顺便说一句,如果您决定 file one,此信息可能会对错误报告有所帮助(我会针对 CoreFoundation 提交它,因为那是实际问题所在)。