- PVSM.RU - https://www.pvsm.ru -
Вы никогда не замечали, что у функции AGGREGATE в Excel у второго набора аргументов неправильное описание аргументов? Фактически, во втором наборе описания аргументов взяты вперемешку из второго и первого наборов. Этот баг точно воспроизводится в Excel 2010, 2013. Мне стало интересно почему так происходит, ведь не могут же в Microsoft так халатно относиться к интерфейсу одного из своих основных продуктов. Итогом стал полный разбор формата файлов локализации MS Office.
После недолгого поиска описаний аргументов функций по содержимому файлов в папке офиса был найден файл c:Program FilesMicrosoft OfficeOffice151033XLINTL32.DLL. Где 1033 – LCID языка локализации (подробнее на msdn [1]).
По беглому взгляду стало понятно, что в принципе я нашёл, что искал. Описания аргументов для функции AGGREGATE для обоих вариантов в файле было правильное. Выходило, что Excel неправильно парсит свой же файл локализации. Тогда было решено написать свой парсер файлов локализации Excel, либо хотя бы разобраться в формате файлов локализации MS Office.
Для начала было решено писать парсер только описаний аргументов и функций, так как по беглому взгляду на файл, что представлен выше, создавалось впечатление, что формат довольно простой – разделителем между текстом служит восклицательный знак, а какой текст что означает, понять можно и опытным путём.
В итоге, после несильно вдумчивого чтения файла, была выявлена такая схема описания аргументов и функций:
По данной схеме был быстро написан парсер, который довольно успешно справлялся со своей работой, но было несколько проблем:
Но в принципе, с большим количеством грязных хаков, удавалось вытащить описания функций для почти всех локализаций.
Осталось решить проблему о том, какой функции какое описание принадлежит. Для этого я опять занялся поиском по содержимому файлов в папках 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 |
После долгих переборов разных вариантов что могут обозначать эти байты, была выделена такая закономерность:
Участок, где записаны данные о том, где какой блок начинается, его размер и т.д. я назвал картой блоков. Для введения терминов, я разбил мысленно файл на три участка: заголовок, карта блоков, собственно блоки с текстом (которые состоят из описания блока и самих данных).
В принципе, этих данных, уже достаточно, чтобы сделать автоматический парсер файла XLLEX.DLL и вытащить названия всех функций на всех языках и много другой информации. Но в процессе возникла одна проблема: лишь очень маленькая часть локализаций хранит данные в UTF-8 формате. Большая часть данных хранится в каких-то совсем непонятных форматах: каждый символ кодируется 1 байтом с некоторым смещением относительно таблицы этого языка в UTF-8. Например, кириллические «С» и «Ч» были записаны как A1 и A7, а в таблице UTF8 у них номера D0A1 и D0A7, но при этом «р» была записана как C0, хотя она должна быть D180.
Чтобы решить эту проблему, я сначала, естественно, попытался понять, как сам Excel переводит строки из такой непонятной кодировки хотя бы в UTF-8. Для этого нужно было сравнить описания блоков для нескольких языков, я взял русскую локализацию и английскую:
Начало описания блока для английской локализации:
Как видим, описания отличаются только первыми двумя байтами. Причём, среди локализаций есть такие, где текст записывался в двухбайтовом Unicode LE. В таких файлах описание было такое: 0000 5602 …
Из этого были сделаны некоторые выводы: первые два байта в описании блока – кодировка. Если первый байт = 0, то текст в этом блоке записан в Unicode LE, и тут всё просто. Если первый байт кодировки = 01, то надо смотреть на второй байт. Если второй байт = 00, то текст закодирован в простой кодировке UTF-8, тут тоже не надо голову ломать. Но что делать, если второй байт не равен 0?
Сначала я просто составлял словарь: значение второго байта – смещение в таблице UTF-8. Мне это быстро наскучило, и я стал искать закономерность. Очень скоро стало понятно, что смещение в таблице UTF-8 можно определить так: offset = (byte2-80h)*4+C0h. Единственная проблема, что для некоторых групп кодировок C0h приходилось менять на другое число.
В итоге, функции преобразования текста стали выглядеть вот так:
#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: Ссылка на Я.Диск [2]
********************
По кодировкам:
Кодировка определяется так:
если второй байт == 0, то это простой Unicode Little Endian.
если второй байт == 1, то элементы закодированы в UTF8 и смещение можно определить по первому байту.
********************
Блоки второго типа бывают ещё нескольких типов
2, 3, 4 — обычные строки
1 — похоже на таблицу WordBasic, там в начале идёт таблица каких-то индексов (может индексов функций WordBasic)
Среди имён функций Excel попались такие, которые нигде не описаны в документации, и к-е использовать в формулах нельзя. Зачем их имена локализованы для меня пока ещё загадка. Вот некоторые из этих функций:
Если кто-нибудь знает как их использовать — напиши в комментариях.
На долгих зимних праздниках мне было ещё скучнее, и я задался целью определить, как Excel вычисляет высоту строки на основе шрифта. После нескольких десятков часов, проведённых в OllyDbg и IDA, были рождены почти 2000 строк кода на C#, которые дают 100% совпадения высоты строки с Excel для всех шрифтов, их размеров и параметров. К тому же было выяснено несколько интересных особенностей внутренностей Excel, но это тема уже для отдельной статьи.
Автор: T-D-K
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/net/80444
Ссылки в тексте:
[1] подробнее на msdn: http://msdn.microsoft.com/en-us/goglobal/bb896001.aspx
[2] Ссылка на Я.Диск: https://yadi.sk/d/DvCTOdj6eAvDe
[3] Источник: http://habrahabr.ru/post/248597/
Нажмите здесь для печати.