Проверка CharacterSet для единственного UnicodeScalar дает странное поведение
Во время работы с CharacterSet
Я столкнулся с интересной проблемой. Из того, что я собрал до сих пор CharacterSet
основан на UnicodeScalar
; Вы можете инициализировать его скалярами и проверить, содержится ли скаляр в наборе. Запрос набора, чтобы узнать, содержит ли он Character
, который может быть составлен из нескольких скалярных значений Юникода, не имеет смысла.
Моя проблема заключается в том, что я тестирую с помощью смайликов, которые представляют собой единственное скалярное значение Юникода (128518 в десятичном виде). Поскольку это единственное скалярное значение Юникода, я бы подумал, что оно будет работать, и вот результаты:
"" == 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
Я удивлен, что это терпит неудачу, и я не уверен, почему это происходит, или как продолжить расследование.
Кто-нибудь может пролить свет на то, почему это может быть?
1 ответ
Исходный код CharacterSet
доступно, на самом деле. Источник для 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
, Исходный код для этого также доступен, хотя только для Yosemite (версии для El Cap и Sierra обе говорят "Скоро"). Тем не менее, код Yosemite, казалось, соответствовал тому, что я видел при разборке на Сьерре. Во всяком случае, код для этого выглядит так:
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>
Основываясь на вышеприведенных структурах, мы можем выяснить, из чего состоит этот набор символов. В этом случае соответствующая часть будет первым байтом cfinfo
от CFRuntimeBase
, которые являются байтами 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
Эд с маской 0x20
, __kCFCharSetClassString
,
На данный момент актерский состав Монти Пайтона кричит "Давай с этим!", Так что давайте пройдемся по источнику для 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
так что это неправда. Emoji, конечно, не в плоскости BMP, так что на самом деле это кажется неправильным для обоих случаев, даже для того, который возвращал правильный результат.
__CFCSetIsRange
проверяет, является ли наш тип __kCFCharSetClassRange
, что верно только в первый раз. Так что это наша точка расхождения. Второй вызов this, который дает неверный результат, возвращается в следующей строке:
return (isAnnexInverted ? true : false);
И так как приложение является NULL
, вызывая isAnnexInverted
чтобы быть ложным, это возвращает ложь.
Что касается того, как это исправить... ну, я не могу. Но теперь мы знаем, почему это произошло. Из того, что я могу сказать, главная проблема в том, что _annex
поле не заполняется при создании набора символов, и, поскольку приложение, кажется, используется для отслеживания символов в плоскостях, отличных от BMP, я думаю, что оно должно присутствовать для обоих наборов символов. Кстати, эта информация, вероятно, будет полезна в отчете об ошибке, если вы решите подать ее (я бы подал ее в CoreFoundation, так как именно в этом проблема).