Pull to refresh

Разбор формата файлов локализации Microsoft Office

Reading time 10 min
Views 9.7K
Вы никогда не замечали, что у функции AGGREGATE в Excel у второго набора аргументов неправильное описание аргументов? Фактически, во втором наборе описания аргументов взяты вперемешку из второго и первого наборов. Этот баг точно воспроизводится в Excel 2010, 2013. Мне стало интересно почему так происходит, ведь не могут же в Microsoft так халатно относиться к интерфейсу одного из своих основных продуктов. Итогом стал полный разбор формата файлов локализации MS Office.


Вариант в лоб


После недолгого поиска описаний аргументов функций по содержимому файлов в папке офиса был найден файл c:\Program Files\Microsoft Office\Office15\1033\XLINTL32.DLL. Где 1033 – LCID языка локализации (подробнее на msdn).


По беглому взгляду стало понятно, что в принципе я нашёл, что искал. Описания аргументов для функции AGGREGATE для обоих вариантов в файле было правильное. Выходило, что Excel неправильно парсит свой же файл локализации. Тогда было решено написать свой парсер файлов локализации Excel, либо хотя бы разобраться в формате файлов локализации MS Office.

Для начала было решено писать парсер только описаний аргументов и функций, так как по беглому взгляду на файл, что представлен выше, создавалось впечатление, что формат довольно простой – разделителем между текстом служит восклицательный знак, а какой текст что означает, понять можно и опытным путём.

В итоге, после несильно вдумчивого чтения файла, была выявлена такая схема описания аргументов и функций:
  1. Все описания записаны в каком-то собственном внутреннем для Excel порядке функций, который не совпадал с порядком функций, описанным в спецификации к формату xls.
  2. Каждое описание функции записано в таком виде: “!”+через запятую описания аргументов функции+[“!”+второй набор аргументов, если он есть]+”!!”+описание самой функции+”!”+описания аргументов, разделяемые “!”
  3. Не все функции имеют описание, есть даже полностью пустые функции, которые в файле записаны так:!!! – и всё.

По данной схеме был быстро написан парсер, который довольно успешно справлялся со своей работой, но было несколько проблем:
  1. Между описаниями некоторых функций, а конкретно между описаниями для функций под номерами 249 и 250, а также между 504 и 505 шли непонятные крякозябры, а потом опять нормальные описания:


  2. Описания некоторых функций не подходили под выбранную схему и для них пришлось писать костыли.
  3. Непонятно, какой функции какое описание принадлежит.
  4. Смещение начала блока описаний функций для разных локализаций было разным, и приходилось этот оффсет узнавать руками и заносить в словарь в парсере. О какой автоматизации тут можно говорить?
  5. В каких-то локализациях описания были в формате UTF-8, в других UTF-16, где-то вообще половина описаний была на UTF-8, другая на UTF-16.

Но в принципе, с большим количеством грязных хаков, удавалось вытащить описания функций для почти всех локализаций.

Спускаемся глубже


Осталось решить проблему о том, какой функции какое описание принадлежит. Для этого я опять занялся поиском по содержимому файлов в папках MS Office, только в этот раз я искал названия функций. И мне повезло: рядом с файлом XLINTL32.DLL с описаниями функций лежал файл XLLEX.DLL, в котором было что-то похожее на названия функций:


Только они шли все как-то подряд и без пробелов. И если для английского языка ещё можно было руками разобрать этот текст на отдельные названия функций, то для арабского или тайского просто так я это сделать не смог бы.
В принципе, тут стало понятно, что пора разобраться уже в формате файлов локализации Excel, либо забить на это дело и лечь спать. Было выбрано первое.

Сначала я заметил, что и описания функций, и названия функций хранятся в dll файлах в ресурсе с именем «1» и типом «234». Вдумчивое изучение дампа ресурса из файла XLLEX.DLL (это тот, что с названиями функций), привело меня к следующему открытию: между участками с нормальным текстом идут участки с кракозябрами, которые должны нести определённый смысл. Тогда было решено эти участки изучить более глубоко, используя WinHEX и калькулятор. Возьмём участок кракозябров, которые идут перед участком с названиями функций:


Первые два байта: 01 00 – пока ещё не знаю, что означают. Вторые два байта 56 02 – если их перевернуть получится 0256, а если ещё перевести из шестнадцатеричной систему в десятичную – получится 598. Ровно столько, сколько имён функций, расположенных ниже в блоке осмысленного текста. Это уже радовало. Смотрим дальше: следующие пары байт, если их поменять местами, похожи на возрастающую последовательность. Так и есть, эти байты являются смещением описания отдельной функции от конца блока кракозябр. В самом деле, по скриншоту из файла XLLEX.DLL видно, что первой функцией идёт COUNT – 5 байт (0005h-0000h), вторая – IF – 2 байта (0007h-0005h), третья – ISNA – 4 байта (000Bh-0007h).

Это всё очень хорошо, но как определить где начинается блок кракозябр, в котором заданы длины названий функций. Ведь в каждой локализации этот блок имеет свой оффсет. Тогда я стал копать заголовок дампа ресурса из файла XLLEX.DLL.


Первые 4 байта – размер ресурса. Далее, меня заинтересовали байты, что находятся по смещению 33h 34h – их значение — 0256 — точно такое же как количество имён функций, записанных в файле. К тому же через каждые 17 байт повторяется 03h, а последние 4 байта в выделенной на скриншоте области — 0E 6F 00 00 как раз равняются числу, равному размеру ресурса = размер выделенной области + 4 + 4 — 1. То есть, по факту, это размер той части файла, где данные.
Теперь можно выписать все байты, которое находятся между повторяющимися 03h и немного их сгруппировать:
03 0F05 00000000 0000 9E1C0000 00000000
03 5602 00000000 0100 51150000 9E1C0000
03 0601 00000000 0200 A00A0000 EF310000
03 0404 00000000 0300 E6310000 8F3C0000
03 3100 00000000 0400 99000000 756E0000
После долгих переборов разных вариантов что могут обозначать эти байты, была выделена такая закономерность:
  • 1 байт – тип блока (в файлах офиса встречаются типы 02, 03, 04 – обычные строки, 01 – похоже на таблицу функций WordBasic, там всё описание идёт в виде имени функции и какого-то индекса у каждой функции).
  • 2 байта – количество элементов в блоке.
  • 4 байта – не знаю. Во всех файлах, что я смотрел, это значения всегда равно 0, может зарезервировано.
  • 2 байта – порядковый номер блока.
  • 4 байта – размер блока
  • 4 байта – смещение блока от конца описания блоков, в случае нашего файла от 7Ah.
Участок, где записаны данные о том, где какой блок начинается, его размер и т.д. я назвал картой блоков. Для введения терминов, я разбил мысленно файл на три участка: заголовок, карта блоков, собственно блоки с текстом (которые состоят из описания блока и самих данных).

Разбираемся с кодировками


В принципе, этих данных, уже достаточно, чтобы сделать автоматический парсер файла XLLEX.DLL и вытащить названия всех функций на всех языках и много другой информации. Но в процессе возникла одна проблема: лишь очень маленькая часть локализаций хранит данные в UTF-8 формате. Большая часть данных хранится в каких-то совсем непонятных форматах: каждый символ кодируется 1 байтом с некоторым смещением относительно таблицы этого языка в UTF-8. Например, кириллические «С» и «Ч» были записаны как A1 и A7, а в таблице UTF8 у них номера D0A1 и D0A7, но при этом «р» была записана как C0, хотя она должна быть D180.

Чтобы решить эту проблему, я сначала, естественно, попытался понять, как сам Excel переводит строки из такой непонятной кодировки хотя бы в UTF-8. Для этого нужно было сравнить описания блоков для нескольких языков, я взял русскую локализацию и английскую:

Начало описания блока для английской локализации:
  • Английская: 0100 5602 0500 … (вторые два байта, как мы выяснили выше – количество элементов в блоке, третьи два байта – длина первого элемента (функция COUNT – 5 байт)…)
  • Российская: 0184 5602 0400 … (вторые два байта – количество элементов, третьи два байта – длина первого элемента (функция СЧЁТ – 4 байта)…)
Как видим, описания отличаются только первыми двумя байтами. Причём, среди локализаций есть такие, где текст записывался в двухбайтовом Unicode LE. В таких файлах описание было такое: 0000 5602 …

Из этого были сделаны некоторые выводы: первые два байта в описании блока – кодировка. Если первый байт = 0, то текст в этом блоке записан в Unicode LE, и тут всё просто. Если первый байт кодировки = 01, то надо смотреть на второй байт. Если второй байт = 00, то текст закодирован в простой кодировке UTF-8, тут тоже не надо голову ломать. Но что делать, если второй байт не равен 0?

Сначала я просто составлял словарь: значение второго байта – смещение в таблице UTF-8. Мне это быстро наскучило, и я стал искать закономерность. Очень скоро стало понятно, что смещение в таблице UTF-8 можно определить так: offset = (byte2-80h)*4+C0h. Единственная проблема, что для некоторых групп кодировок C0h приходилось менять на другое число.

В итоге, функции преобразования текста стали выглядеть вот так:
Код C#
        #region Convert
        int GetCharSize(int blockIndex) {
            if(block2Encoding[blockIndex][0] == 0)
                return 2;
            if(block2Encoding[blockIndex][0] == 1)
                return 1;
            if (block2Encoding[blockIndex][0] == 2)
                return 2;
            if (block2Encoding[blockIndex][0] == 3)
                return 1;
            return 1;
        }

        byte[] Convert(byte[] array, int blockIndex) {
            byte encodingByte1 = block2Encoding[blockIndex][0];
            byte encodingByte2 = block2Encoding[blockIndex][1];
            if(encodingByte1 == 0 || encodingByte1 == 2)
                return Convert0000(array);
            if(encodingByte2 < 0x80)
                return ConvertFromUTF8(array, 0x00, 0xC2);
            int d = encodingByte2 - 0x80;
            d *= 4;
            byte byte1;
            byte byte2;
            if(d < 0x20) {
                byte1 = 0;
                byte2 = (byte) (0xC0 + d);
            }
            else if(d < 0x40) {
                byte1 = 0xE0;
                byte2 = (byte) (0xA0 + (d - 0x20));
            }
            else {
                d -= 0x40;
                byte1 = (byte) (0xE1 + d / 0x40);
                byte2 = (byte) (0x80 + d % 0x40);
            }
            return ConvertFromUTF8(array, byte1, byte2);

        }

        byte[] ConvertFromUTF8(byte[] array, byte byte1, byte byte2) {
            List<byte> result = new List<byte>();
            foreach(byte b in array) {
                if(b <= 0xFF / 2)
                    result.Add(b);
                else {
                    if(byte1 != 0)
                        result.Add(byte1);
                    byte d = (byte) (byte2 + (b - 1) / 0xBF);
                    result.Add(d);
                    d = b;
                    if(b >= 0xC0) {
                        d = (byte) (b - 0xC0 + 0x80);
                    }
                    result.Add(d);
                }
            }
            return result.ToArray();
        }

        byte[] Convert0000(byte[] array) {
            return Encoding.Convert(Encoding.Unicode, Encoding.UTF8, array);
        }
        #endregion

Докапываемся до самой сути


После всего этого, можно было уже точно и правильно вытащить весь локализованный текст из файла XLLEX.DLL, но этот способ совершенно не подходил для файла с описаниями функций и аргументов XLINTL32.DLL. Тут пришлось начинать всё почти с самого начала, но уже было проще.

Для начала в файле XLINTL32.DLL я попытался найти что-то уже знакомое и похожее на данные из файла XLLEX.DLL. Знакомая картинка начиналась по смещению 0459h:



Т.е. начиная с 04B1h были описания блоков, такие же как в файле XLLEX.DLL, но выше этого смещения всё было какое-то непонятное. И не весь текст из ресурса подчинялся правилам, которое были выведены на основе разбора файла XLLEX.DLL.

Было решено в дальнейшем те блоки, что я уже научился распознавать называть блоками второго типа, а те, что ещё не умею – блоками первого типа, т.к. они шли в файле XLINTL32.DLL выше блоков второго типа.

Текст блоков первого типа начинался почти сразу после окончания карты блоков второго типа, осталось найти, где в файле находится карта блоков первого типа, и как в самих блоках первого типа определить разделитель текста. Для изучения был выбран этот блок:



В нём чётко видны такие строки: “Cut, copy, and paste”, “Print”, “For charts” и т.д. Кроме того, в hex кодах видна «характерная» лесенка из нулей и увеличивающихся значений. Первые два значения в этой лесенке — 46h и 6Eh – разница между ними в десятичном виде 40, т.к. текст явно задан в Unicode LE, то длина “Cut, copy, and paste” – будет 20*2 = 40. Сходится. Проверим другую пару значений: 78h-6Eh=10/2=5 – аккурат длина “Print”. Перепишем красиво все байты от смещения 07BE66h до начала осмысленного текста:
00 46000000
00 6E000000
00 78000000

FF E2020000

Общая длина получившейся выписки составляет 07BEABh — 07BE66h + 1 = 46h — где-то это уже было. Выходит, что описания элементов в блоке первого типа выглядят так: 1 байт тип элемента, 4 байта – смещение элемента относительно начала этого блока. Как позже выяснилось, типы элементов в блоке первого типа бывают 00h – обычный текст в Unicode, 0Ah – какие-то непонятные кракозябры, FFh – последний элемент в этом блоке.

Теперь осталось последнее: разобраться с заголовком ресурса и выяснить, как определяются смещения для всех блоков.

Для начала я запомнил, что описание всех блоков второго типа заканчиваются по смещению 0A67h, а начинаются по смещению 0459h, итого длина описания блоков второго типа 0A67h-0459h+1 = 060Fh, а по адресу 0455h лежит четырёхбайтовое число 060Bh: 060Bh+4 = 060Fh. Выходит, по адресу 455h записана длина участка описания блоков второго типа.

Для того, чтобы понять, как описаны смещения блоков первого типа от начала ресурса, было решено составить для каждого блока первого типа таблицу смещения начала этого блока и его длину.

Первый блок первого типа начинается там, где заканчиваются описания блоков второго типа – 0A68h.
Смещение Длина
0A68h 011Ah
0B82h 00CEh
0C50h 0148h
А между началом ресурса и смещением 0455h находились байты, очень напоминающие возрастающую последовательность:



Попробуем вычесть из 01E8h (смещение 25h) число 011Ah (смещение 21h): 01E8h-011Ah=CEh, как раз длина второго блока. Ради интереса: вычтем 0330h (смещение 29h) из 01E8h (смещение 25h): 0330h-01E8=0148h, а 011Ah – похоже на длину первого блока. Выходит, что со смещения 1Dh идут описания смещений блоков первого типа. Записаны они в виде смещений начала блока от конца описаний блоков второго типа (или начала раздела с содержимым блоков – кому как удобнее). Осталось понять, что за байты лежат между 04h и 1D. Если вычесть 1D (начало описания смещений блоков первого типа) и 0455h (смещение по которому хранится длина описания блоков второго типа, т.е. заканчиваются описания блоков первого типа): 0455h-1D=0438h, как раз такое число лежит по смещению 19h. Что располагается в остальных двадцати одном байте между 04h и 19h – для меня загадка. Да и разбираться особо уже не хотелось, т.к. во всех файлах локализации офиса это смещение одинаковое.

Моя программа для чтения файлов локализации Microsoft Office: Ссылка на Я.Диск

UPD: 16.01.2016
Выяснилось, что блоки первого типа иногда кодируются дополнительным словарём, к-й записывается в самый конец файла. Видимо это сделано для сокращения размера файла, т.к. в словаре может быть много букв представлены одним байтом. Собственно по ссылке теперь обновлённый исходный код.

Короткая спецификация структуры файла
4 байта — размер ресурса
21 байт – не известность
4 байта — количество блоков 1 типа * 4 = количество байт, которыми описывается карта блоков 1 типа
*Начало описания карты блоков первого типа*
4 байта — оффсет блока 1 типа от конца описания блоков. Разница между двумя соседними значениями — длина блока 1 типа.
*Конец описания карты блоков первого типа*
4 байта — количество байт, которыми описывается карта блоков 2 типа = количество блоков 2 типа * 17 байт
*Начало описания карты блоков второго типа*
1 байт — тип блока
2 байта — количество элементов в блоке 2 типа
4 байта — не знаю. По-моему всегда 0, может зарезервировано.
2 байта — порядковый номер блока 2
4 байта — длина блока второго типа
4 байта — оффсет блока второго типа от конца описания блоков
*Конец описания карты блоков второго типа*
4 байта — длина данных для блоков 2 типа
*Блоки первого типа*
1 байт — тип элемента — есди FF — этот элемент последний в этом блоке, если 00 — обычный текст в Unicode, если 0A — хз.
4 байта — смещение элемента относительно начала этого блока
Далее элементы
*Конец блоков первого типа*
*Блоки второго типа*
1 байт — первый байт кодировки
1 байт — второй байт кодировки
2 байта — количество элементов в блоке
*Карта элементов блока второго типа*
2 байта — смещение относительно начала элементов блока. Начинается со второго элемента. Если оффсет перелезает за FFFF, то после смещения добавляются ещё два байта = сколько раз по FFFF надо взять.
*Конец карты элементов блока второго типа*
*Конец блоков второго типа*

********************
По кодировкам:
Кодировка определяется так:
если второй байт == 0, то это простой Unicode Little Endian.
если второй байт == 1, то элементы закодированы в UTF8 и смещение можно определить по первому байту.
********************
Блоки второго типа бывают ещё нескольких типов
2, 3, 4 — обычные строки
1 — похоже на таблицу WordBasic, там в начале идёт таблица каких-то индексов (может индексов функций WordBasic)

Это интересно


Среди имён функций Excel попались такие, которые нигде не описаны в документации, и к-е использовать в формулах нельзя. Зачем их имена локализованы для меня пока ещё загадка. Вот некоторые из этих функций:
  • GOTO(reference);
  • HALT(cancel_close);
  • ECHO(logical);
  • WINDOWS(type_num, match_text);
  • INPUT(message_text, type_num, title_text, default, x_pos, y_pos, help_ref);
  • ADD.TOOLBAR(bar_name, tool_ref).
Если кто-нибудь знает как их использовать — напиши в комментариях.

P.S.


На долгих зимних праздниках мне было ещё скучнее, и я задался целью определить, как Excel вычисляет высоту строки на основе шрифта. После нескольких десятков часов, проведённых в OllyDbg и IDA, были рождены почти 2000 строк кода на C#, которые дают 100% совпадения высоты строки с Excel для всех шрифтов, их размеров и параметров. К тому же было выяснено несколько интересных особенностей внутренностей Excel, но это тема уже для отдельной статьи.
Tags:
Hubs:
+21
Comments 4
Comments Comments 4

Articles