Проверка 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, так как именно в этом проблема).

Другие вопросы по тегам