Pull to refresh

Внутренности вордовских файлов: просто ужас

Reading time 9 min
Views 55K
О сложности и жуткости вордовских файлов давно ходили легенды. Известно было, что формат этот крайне запутанный, а к тому же еще и полностью засекреченный, так что о половине тамошних полей можно было только догадываться.

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

А теперь вот жизнь заставила (или подкинула возможность) все-таки разобраться во внутренностях всем хорошо известных документов, тем более, что в Штирлица теперь играть не обязательно, достаточно скачать с сайта «Майкрософта» официальные спецификации.

Что тут можно сказать? Невольно вспоминается старый пошлый анекдот: ну ужас. Ну просто ужас, но ведь не ужас-ужас-ужас.

Слава богу, что я разбирал эти файлы на Перле, а не на каком-нибудь автокоде Си. Высокий уровень языка и куча готовых библиотек (например, для чтения разных кодовых страниц) — это дар божий.

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

Итак, как же устроены вордовские файлы?

Контейнер

Начнем с того, что это совсем не вордовские файлы, а некий универсальный контейнер, в который упакованы собственно документы. В такой контейнер засунуты все файлы Офиса, а, может быть, и еще что-нибудь.

Формат контейнера называется по-разному — docfile, ole storage, compound document file. Уже сам разнобой в названиях намекает на то, что он не сильно-то и нужен, поскольку действительно полезная вещь обычно имеет одно и четкое название. Основная его идея — иметь возможность запихать в один файл несколько других. Самым разумным было бы (как и сделали в OpenOffice) упаковать все в архив ZIP (можно без архивации, если она не нужна). Формат известен огромному множеству программ, компактен, легко разбирается. Но в «Майкрософте», очевидно, процветает синдром «Not invented here». Главное — изобрести велосипед, пусть и трехколесный, но собственный.

В принципе, формат OLE Storage достаточно разумен и не производит впечатление чего-то совсем идиотского. Но… он совершенно не нужен. Фактически, это что-то типа файловой системы FAT16, засунутой внутрь отдельного файла. Это не то что стрельба из пушки по воробьям, а истребление этих самых воробьев ядерными торпедами. В документе, внутри которого лежит несколько файлов, не нужна файловая система, находящаяся не менее чем на уровне ФС времен ДОС.

Итак, файлы CDF (как их называет юниксовская утилитка file) начинаются с заголовка. В заголовке отведено место под первую сотню записей ТРФ (таблицы размещения файлов, в просторечии — ФАТ). ФАТ там самый натуральный, очень похож на то, что можно найти на досовских дискетках. Остальные записи ТРФ лежат в отдельных секторах, соединенных связанным списком. Дополнительные сектора бывают только в больших (>7мб) файлах.

По табличке ФАТ/ТРФ можно собрать содержимое любого внутреннего файла (в терминологии МС он называется потоком, но это только запутывает дело), если знать, с какого блока он начинается. Начальные блоки разных структур написаны, естественно, в заголовке. Дальше из таблицы можно вытянуть всю цепочку секторов, в котором записано содержимое этого псевдофайла.

В частности, у CDF есть корневой каталог, физически размазанный по куче секторов. Это самый настоящий каталог, опять-таки, очень напоминающий старый досовский. Правда, для эффективности (или для выпендрежа) он выполнен не просто линейным списком, а сбалансированным двоичным деревом. Это значит, что в него без потери эффективности поиска можно записывать десятки тысяч отдельных записей. Зачем это нужно в файле, в котором записей бывает обычно штук пять, ну иногда, двадцать, ну максимум сто штук (презентация с огромным количество картинок) — знают только в Редмонде. Кстати, имена файлов в каталоге хранятся в UTF16 — тоже на всякий случай.

По каталогу можно определить начальный сектор любого файла и с помощью ТРФ вытянуть всю его цепочку размещения.

Но это еще не все.

Поскольку размер блока немаленький (обычно 512 байт, по спецификации возможно также 4096), то при хранении мелких псевдофайлов, теоретически можно потерять много свободного места. Поэтому существует отдельное хранилище, поделенное на блочки по 64 байта. Хранилище опять-таки вытягивается по цепочке ФАТ.

Чтобы указать, какие блочки какому файлу принадлежат, существует отдельная табличка ФАТ или, вернее сказать, миниФАТ.

Итак, чтобы добраться до вордовского документа, надо сделать следующее:

1. Прочитать заголовок CDF
2. Загрузить в память ФАТ — таблицу размещения файлов, собрав ее по цепочке секторов.
3. Загрузить табличку МиниФАТ, собрав ее по цепочке ТРФ
4. Загрузить хранилище блочков, собрав ее по цепочке ТРФ
5. Загрузить корневой каталог, собрав ее по цепочке ТРФ
6. Разобрать каталог и преобразовать его во что-то читаемое
7. Найти в каталоге запись WordDocument
8. Если это маленький файл, то собрать его с помощью миниТРФ из хранилища для блочков.
9. Если большой, то вытянуть с диска сектора по цепочке ТРФ.

Уф, вроде все.

Каждый шаг сам по себе не особенно сложен, но в совокупности они вызывают исключительно недоумение. Зачем такие сложности? Почему нельзя было разместить после заголовка обыкновенный линейный каталог, а после него непрерывно, друг за другом записывать внутренние файлы?

Единственное, что можно предположить — все это сделано для того, чтобы была возможность дописывать подфайлы, не трогая начало основного файла. Надо заметить, что, во-первых, это не сильно востребованная операция, поскольку все программы обычно записывают документы от начала до конца в один проход. Исключение составляет только MS Word и то только в пресловутом режиме быстрого сохранения, проклятом пользователями. А во-вторых, даже в этих условиях все равно не получится не трогать начало основного файла, поскольку надо обновлять каталоги, ТРФ и заголовки.

В общем, «Майкрософт» в своем амплуа. Зачем делать просто, если можно сложно и запутанно?

WordDocument

Формат CDF при всей своей монструозности хотя бы логичен и не очень сложен (если сравнивать с остальным содержимым вордовского документа). Его описание занимает всего каких-то двадцать страниц — тьфу по сравнению с 300 страницами формата Ворда.

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

Достаточно посмотреть на заголовок, который занимает чуть ли не треть файла. Заголовков целых три. Сначала идет один небольшой, в котором половина записей зияет дырами «Reserved» или «Not used». Раньше, в мезозое, там явно что-то лежало, но потом было выкинуто на свалку истории. Здесь же имеется версия записавшей программы, по которой в коде, похоже, выполняется огромный switch/case.

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

После этого идет третий заголовок, на этот раз современный, из длинных слов (32 бита). Он немерянной длины, в начале тоже указывает количество записей с прицелом на дальнейшее расширение, и в основном представляет собой список, где искать различные таблицы и куски файла — пары начало/размер. Сами таблицы, кстати, лежат не здесь, а в отдельном псевдофайле CDF под названием 0Table или 1Table (возможны варианты).

В первом заголовке написана длина самого текста и его начало. Очевидно, что во времена царя Гороха именно так его и можно было прочитать. Текст лежал одним большим куском. Забавно, что можно читать его так и сейчас, но… не всегда! На десять читаемых файлов найдется такой, у которого в середине окажутся невразумительные куски, в конце — сноски, которых там быть не должно, а в самом начале — большой кусок текста, который стерли в прошлом году. Кроме того, половина файла окажется написана китайскими иероглифами. Прискорбно заметить, что известная утилита catdoc Витуса Вагнера в некоторых случаях именно такие результаты и дает, из чего можно сделать вывод, что формат она разбирает недокорректно.

Жизнь на самом деле гораздо сложнее. Когда-то в файлах действительно был только текст, однако со временем под давлением пользователей и маркетинга накапливались различные «фичи». Под них отвели отдельные потоки — под простые сноски, под сноски концевые, под колонтитулы, под какие-то textbox (то еще извращение — на вид текст, но не текст. Назначение толком не ясно).

Начала этих потоков указаны в специальных местах заголовка, но самый первый заголовок почему-то показывает общую длину — не самого текста, а текста плюс все этих извращений. Вот и первая причина, почему в вывод многих утилит попадают надписи типа Page 1.

Где-то в архее в редактор добавили быстрое сохранение. Смысл его в том, что файл целиком не переписывается, а добавления и изменения просто дописываются в его конец, что теоретически должно быть быстрее. Предполагалось радовать этим пользователей, но фактически они остались недовольны. Особой разницы в скорости записи при этом не получается, но в файле образуется много мусора, причем из кусков, которые теоретически уже стерты. Если там была какая-нибудь секретная информация, то простым просмотром дампа файла ее можно легко обнаружить.

Для поддержки быстрого сохранения была заведена особая таблица огрызков (piece table), в которую записывается начало каждого куска и его адрес в файле. Длины нет, но ее можно высчитать, вычтя начало текущего куска из начала следующего. Однако тут тоже надо быть осторожным, поскольку огрызки перечисляются из всех потоков. Слава богу, что они идут в определенном порядке, поэтому, зная общую длину текста, легко вовремя остановиться.

Теоретически, этот сложный формат задействован только, если в заголовке установлен специальный флажок fComplex. Но… Вот на этом очередном «но» тоже прокалываются многие конверторы.

Уже в наше время в документы добавили возможность записи в Юникоде. При этом встала проблема (как по мне, надуманная): а ведь файлы получаются ровно в два раза длиннее. Поскольку ПО разрабатывают американцы, которые в душе вообще не верят в существование других азбук, и тайно считают, что всякие странные буквы бывают только в диссертациях про Древнюю Грецию, да и там встречаются только иногда, первое, что пришло им на ум — отделить чистые символы ASCII от грязных юникодовских. Первые писать по байту на символ, вторые — как получится.

Из этой идеи возникла, например, элегантная кодировка UTF-8, где двухбайтовые символы кодируются хитрыми последовательностями в духе кодирования Хаффмана. В «Майкрософте» сделали то же самое, только не так красиво. Раз уж у нас есть таблица огрызков, то запишем туда заодно и какие куски текста написаны в чистом ASCII (на самом деле сp1252), а какие — на всякого рода невразумительных алфавитах, требующих Юникода и, соответственно два байта на символ. Поэтому нынешние файлы всегда нужно разбирать с помощью таблицы кусков, невзирая на всякие там флажки. Юникодовские фрагменты там берутся как есть, только надо учитывать, что количество читаемых байтов должно быть в два раза больше количества читаемых символов. Однобайтовые фрагменты отмечаются в адресе установленным вторым слева старшим битом (почему не первым?). Чтобы узнать настоящий адрес, нужно этот бит сбросить, а адрес разделить на два (!).

Если учесть, что сама эта таблица огрызков тоже занимает место, а еще больше места в файле занимают разные двоичные деревья и таблички цепочек секторов от формата CDF, то размеры экономии текста на символах Юникода не поразят воображения даже в древнегреческих диссертациях. О файлах на великом и могучем языке и говорить нечего. Положили бы все в UTF-16 и не страдали. Ну заархивировали бы поток, раз уж так жаба давит.

После героических усилий по чтению текста, в нем самом, как ни странно, нет ничего сложного. Обычный текст (с поправкой на кодировку), кое-какие коды ниже пробела играют служебную роль. Например, 0х9 обозначает, как и положено, табуляцию, 0хА — конец страницы, 0х7 — конец ячейки таблицы и т.д. Единственная тонкость связана с полями. Начало содержимого поля обозначается как 0х13, конец поля — 0х15, имя и параметры поля отделяются символом 0х14 от того, что, собственно, видно в тексте пользователю. Но… Вторая часть может иметь в себе вложенное поле, чего многие программы не учитывают. В результате в тексте остаются огрызки вроде INCLUDEPICTURE или PAGEREF *.

Впрочем, есть еще одна мелкая пакость. Некоторые символы могут означать что-нибудь совсем другое, вроде текущей даты. Чтобы понять, простой это символ или нет, надо разбирать таблицы свойств символов, о которых ниже. Каюсь, я просто вырезал все символы с кодом ниже пробела, что не совсем достаточно, но дешево, быстро и практично.

Выдрав текст, дальше в формат я углубляться не стал. Это уже занятие для молодых и сильных духом — разобрать все эти таблицы с такими многообещающими названиями как CHP, PAPX, SHST, PLCF и все в том же духе. Занятие совсем уже для титанов — вопроизвести форматирование в точности, как это делает сам Ворд.

Кратко изложу только, что все хранится в специальных таблицах, входом в которые служит адрес символа с начала потока. Стили лежат в длинных списках, изменения в стилях — в специальных списках исключений. Локальные изменения стиля, например, при редактировании абзаца или символа хранятся в таблицах как специальные команды по изменению родительских таблиц стилей. Сами команды очень напоминают команды виртуальной машины от типичной игры-квеста.

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

Думаю, что «Майкрософт» столько лет его не открывала не потому, что боялась конкуренции, а просто потому что было… стыдно.

Дополнительная литература:


Официальное описание формата:
http://msdn.microsoft.com/en-us/library/cc313153(office.12).aspx

Дж. Спольски, «Почему форматы Офиса так сложны?»
http://www.joelonsoftware.com/items/2008/02/19.html
Tags:
Hubs:
+151
Comments 211
Comments Comments 211

Articles