Pull to refresh

.Net, UTF-16 и регулярные выражения

Reading time3 min
Views2.3K
Как-то мне понадобилось проверить, является ли XML-имя правильным. Что может быть проще? Смотрим стандарт, где четко описано, какими символами может имя начинатся, а какими — продолжаться, все просто и понятно:

[4] NameStartChar ::= ":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] | [#x370-#x37D] | [#x37F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF]
[4a] NameChar ::= NameStartChar | "-" | "." | [0-9] | #xB7 | [#x0300-#x036F] | [#x203F-#x2040]
[5] Name ::= NameStartChar (NameChar)*


Практически готовое регулярное выражение, легкая обработка напильником Ctrl+H…

    public const string NameStartCharPattern = @"\:|[A-Z]|_|[a-z]|[\u00C0-\u00D6]|[\u00D8-\u00F6]|[\u00F8-\u02FF]|[\u0370-\u037D]|[\u037F-\u1FFF]|[\u200C-\u200D]|[\u2070-\u218F]|[\u2C00-\u2FEF]|[\u3001-\uD7FF]|[\uF900-\uFDCF]|[\uFDF0-\uFFFD]|[\u10000-\uEFFFF]";
    public const string NameCharPattern = NameStartCharPattern + @"|-|\.|[0-9]|\u00B7|[\u0300-\u036F]|[\u203F-\u2040]";
    public const string NamePattern = @"(?:" + NameStartCharPattern + @")(?:" + NameCharPattern + @")*";

* This source code was highlighted with Source Code Highlighter.


Пишем тест…
Assert.That(Regex.Match("4a", Patterns.NamePattern), Is.False);
* This source code was highlighted with Source Code Highlighter.

Чисто, просто, понятно… Упал!

Корнем зла оказался последний компонент в первой строке: [\u10000-\uEFFFF]. Он ловит все символы, хотя и не должен… Стоп, как ловит? У нас же UTF-16, символ ограничен двумя байтами?.. Или не ограничен?..

Мне пришлось срочно занятся ликвидацией собственной безграмотности в области кодировок, и результаты своего образования я привожу в короткой форме здесь. Если кому-то эти факты окажутся давно знакомыми — смело пропускайте следующий абзац.

Оказывается, Unicode имеет возможность кодировать гораздо больше, чем 65536 символов. Символы Unicode поделены на так называемые плоскости, и каждая из них ёмкостью в 0x10000 символов. Всего стандарт определяет их 17. И такое «кривое» с точки зрения программиста число здесь неспроста: по сути мы имеем одну плоскость, которая обрабатывается одним способом, и 16 — другим. Первая, так называемая базовая многоязыковая плоскость, известная также под аббревиатурой BMP, содержит подавляющее большинство всех используемых на сегодня символов. Все символы из неё при кодировании в UTF-16 записываются двумя байтами, одним словом, прямо соответствующими коду символа в них. В этой же плоскости определен специальный диапазон кодов, 0xD800-0xDFFF. Он содержит 2048 значений, которые называются суррогатами. Сами по себе эти значения в UTF-16 встречатся не могут, только парами — два слова (два по два байта) задают значение из следующих шестнадцати панелей следующим образом: от кода символа отнимается 0x10000, что дает нам чистое двадцатибитное число. Эти 20 бит пишутся по 10 в первое и второе слово, занимая таким образом 2048 выделенных кодов. Более того, поскольку первое слово пишется с префиксом 0b110110 (давая при этом значения 0xD800-0xDBFF, называемые высоким или ведущим суррогатом), а второе — 0b110111(0xDC00-0xDFFF, соответственно заключительный или низкий суррогат), это гарантирует однозначное определение предназначения каждого слова вне зависимости от контекста.

… Так вот, казалось бы при чем тут .Net? А при том, что хотя в нем предусмотрены инструменты для работы с суррогатами, движок регулярных выражений игнорирует их. Тоесть игнорирует вообще, работая с с ними как парами символов. Как обычно в таких случаях, я был не первым, нашедшим эту проблему. Опять-таки, как обычно, вердикт Microsoft — Won't fix.

Значит, придется как-то с этим жить. Как предложено в багрепорте вызывать через PInvoke сторонний движок — из пушки по воробьям. Вторая идея — выбросить к черту вообще поддержку этих суррогатов была соблазнительной, но я решил не сдаватся… И тут вдруг понял, что баг можна использовать как фичу!

Структура группы, которая должна работать с суррогатами в нашем случае очень проста — по сути она разрешает любые символы из первых 14 плоскостей, запрещая две последние… Тоесть, запрещает некоторый диапазон значений из области высокого суррогата, и мы можем заменить наше выражение на следующее:
[\u10000-\uEFFFF] -> (?:[\uD800-\uDB7F][\uDC00-\uDFFF])

Этот способ не очень универсален, и задавать ним более узкие диапазоны символов будет ужасно неудобно, мне он показался красивым, и поэтому я решил им с вами поделится.
Tags:
Hubs:
+25
Comments18

Articles