# Чёрная дыра бэктрекинга Некоторые регулярные выражения, с виду являясь простыми, могут выполняться оооочень долго, и даже "подвешивать" интерпретатор JavaScript. Рано или поздно, с этим сталкивается любой разработчик, потому что нечаянно создать такое регулярное выражение -- легче лёгкого. Типична ситуация, когда регулярное выражение до поры до времени работает нормально, и вдруг на каком-то тексте как начнёт "подвешивать" интерпретатор и есть 100% процессора. Это может стать уязвимостью. Например, если JavaScript выполняется на сервере, то при разборе данных, присланных посетителем, он может зависнуть, если использует подобный регэксп. На клиенте тоже возможно подобное, при использовании регэкспа для подсветки синтаксиса. Такие уязвимости "убивали" почтовые сервера и системы обмена сообщениями и до появления JavaScript, и наверно будут "убивать" и после его исчезновения. Так что мы просто обязаны с ними разобраться. [cut] ## Пример План изложения у нас будет таким: 1. Сначала посмотрим на проблему в реальной ситуации. 2. Потом упростим реальную ситуацию до "корней" и увидим, откуда она берётся. Рассмотрим, например, поиск по HTML. Мы хотим найти теги с атрибутами, то есть совпадения вида `subject:`. Самый простой способ это сделать -- `pattern:<[^>]*>`. Но он же и не совсем корректный, так как тег может выглядеть так: `subject:`. То есть, внутри "закавыченного" атрибута может быть символ `>`. Простейший регэксп на нём остановится и найдёт `match:" href="#"> ``` А нам нужен весь тег. Для того, чтобы правильно обрабатывать такие ситуации, нужно учесть их в регулярном выражении. Оно будет иметь вид `pattern:<тег (ключ=значение)*>`. Если перевести на язык регэкспов, то: `pattern:<\w+(\s*\w+=(\w+|"[^"]*")\s*)*>`: 1. `pattern:<\w+` -- начало тега 2. `pattern:(\s*\w+=(\w+|"[^"]*")\s*)*` -- произвольное количество пар вида `слово=значение`, где "значение" может быть также словом `pattern:\w+`, либо строкой в кавычках `pattern:"[^"]*"`. Мы пока не учитываем все детали грамматики HTML, ведь строки возможны и в 'одинарных' кавычках, но на данный момент этого достаточно. Главное, что регулярное выражение получилось в меру простым и понятным. Испытаем полученный регэксп в действии: ```js run var reg = /<\w+(\s*\w+=(\w+|"[^"]*")\s*)*>/g; var str='...... ...'; alert( str.match(reg) ); // , ``` Отлично, всё работает! Нашло как длинный тег `match:`, так и одинокий `match:`. А теперь -- демонстрация проблемы. Если запустить пример ниже, то он может подвесить браузер: ```js run var reg = /<\w+(\s*\w+=(\w+|"[^"]*")\s*)*>/g; var str = "/g; var str = "` в строке `subject:`, оказывается не подходящей. Движок честно отступает, пробует другую комбинацию -- и так далее. ## Что делать? Проблема -- в сверхмноговариантном переборе. Движок регулярных выражений перебирает кучу возможных вариантов скобок там, где это не нужно. Например, в регэкспе `pattern:(\d+)*$` нам (людям) очевидно, что в `pattern:(\d+)` откатываться не нужно. От того, что вместо одного `pattern:\d+` у нас два независимых `pattern:\d+\d+`, ничего не изменится. Без разницы: ``` \d+........ (123456789)z \d+...\d+.... (1234)(56789)z ``` Если вернуться к более реальному примеру `pattern:<(\s*\w+=\w+\s*)*>` то сам алгоритм поиска, который у нас в голове, предусматривает, что мы "просто" ищем тег, а потом пары `атрибут=значение` (сколько получится). Никакого "отката" здесь не нужно. В современных регулярных выражениях для решения этой проблемы придумали "possessive" (сверхжадные? неоткатные? точный перевод пока не устоялся) квантификаторы, которые вообще не используют бэктрегинг. То есть, они даже проще, чем "жадные" -- берут максимальное количество символов и всё. Поиск продолжается дальше. При несовпадении никакого возврата не происходит. Это, с одной стороны, уменьшает количество возможных результатов, но, с другой стороны, в ряде случаев очевидно, что возврат (уменьшение количество повторений квантификатора) результата не даст. А только потратит время, что как раз и доставляет проблемы. Как раз такие ситуации и описаны выше. Есть и другое средство -- "атомарные скобочные группы", которые запрещают перебор внутри скобок, по сути позволяя добиваться того же, что и сверхжадные квантификаторы, К сожалению, в JavaScript они не поддерживаются. Однако, можно получить подобный эффект при помощи предпросмотра. Подробное описание соответствия с учётом синтаксиса сверхжадных квантификаторов и атомарных групп есть в статьях [Regex: Emulate Atomic Grouping (and Possessive Quantifiers) with LookAhead](http://instanceof.me/post/52245507631/regex-emulate-atomic-grouping-with-lookahead) и [Mimicking Atomic Groups](http://blog.stevenlevithan.com/archives/mimic-atomic-groups), здесь же мы останемся в рамках синтаксиса JavaScript. Взятие максимального количества повторений `a+` без отката выглядит так: `pattern:(?=(a+))\1`. То есть, иными словами, предпросмотр `pattern:?=` ищет максимальное количество повторений `pattern:a+`, доступных с текущей позиции. А затем они "берутся в результат" обратной ссылкой `pattern:\1`. Дальнейший поиск -- после найденных повторений. Откат в этой логике в принципе не предусмотрен, поскольку предпросмотр "откатываться" не умеет. То есть, если предпросмотр нашёл 5 штук `pattern:a+`, и в результате поиск не удался, то он не будет откатываться на 4 повторения. Эта возможность в предпросмотре отсутствует, а в данном случае она как раз и не нужна. Исправим регэксп для поиска тега с атрибутами `pattern:<\w+(\s*\w+=(\w+|"[^"]*")\s*)*>`, описанный в начале главы. Используем предпросмотр, чтобы запретить откат на меньшее количество пар `атрибут=значение`: ```js run // регэксп для пары атрибут=значение var attr = /(\s*\w+=(\w+|"[^"]*")\s*)/ // используем его внутри регэкспа для тега var reg = new RegExp('<\\w+(?=(' + attr.source + '*))\\1>', 'g'); var good = '...... ...'; var bad = ", alert( bad.match(reg) ); // null (нет результатов, быстро) ``` Отлично, всё работает! Нашло как длинный тег `match:`, так и одинокий `match:`.