
Пустая диаграмма — шесть дней работы
Я пишу “Развертку” — утилиту, которая разбирает файл конфигурации 1С:Предприятия в XML и собирает его обратно. Главное требование: результат должен совпадать с файлом, который создает сама платформа, с точностью до бита. “Почти такой же” не подходит.
В табличных документах типовых конфигураций встречаются диаграммы Ганта. Их немного, и все они пустые: точки, серии и интервалы появляются только во время работы программы.
Казалось бы, что сложного в пустой диаграмме? Но на ее разбор ушло шесть дней.
Рассказываю, что оказалось внутри.
Диаграмма внутри диаграммы
Диаграмма Ганта устроена как луковица: снимаешь один слой, а под ним следующий, и на каждом есть повод прослезиться.
Снаружи это рисунок табличного документа особого вида. Внутри — обычная диаграмма с легендой, палитрой, шкалами и рамками. Поверх нее добавлены элементы Ганта: шкала времени, интервалы, связи между ними и фоновые интервалы.
|
Слой |
Что содержит |
|---|---|
|
Обертка Ганта |
Шкалу времени, интервалы, связи и фоновые интервалы |
|
Обычная диаграмма |
Серии, точки, легенду, заголовок, палитру и рамки |
|
Холст 480 x 360 |
Координаты областей диаграммы |
Формат растет вместе с платформой. В версии записи 17 у обертки Ганта 27 полей, в версии 18 — 31, в версии 19 — 33. Новые поля добавляются в конец: например, нужно ли показывать текст точек и где его размещать. Начало записи остается прежним.
Даже пустые точки и серии содержат данные: ключи, служебное число baseData, шрифт, картинку, цвет штриховки. В XML это выглядит как десяток нулей, а во внутренней записи превращается во вложенные списки глубиной в три уровня.
Поэтому разбирать Ганта пришлось изнутри: сначала обычную диаграмму, затем ее холст и только потом обертку.
Холст 480 x 360

Первая загадка — координаты.
В XML положение области построения, легенды и заголовка задается долями от размера диаграммы. Например, левый край области построения — 0.1675, правый — 0.8325.
Во внутренней записи тоже хранятся доли, но другие: ровно 1/6 и 5/6.
Ответ нашелся в коде. Платформа размещает элементы на воображаемом холсте размером 480 x 360 точек, независимо от реального размера рисунка. Сначала она переводит долю в координату на этом холсте и округляет ее до целой точки. Затем переводит обратно в долю:
0.1675 x 480 = 80.4После округления: 8080 / 480 = 1/6
Каждая координата рамки сохраняется дважды: как двоичное число с плавающей точкой и как десятичное число с 27 знаками после запятой. Для одной шестой эти значения выглядят так:
1.666666666666667e-10.166666666666666666666666667
До этого открытия диаграмма Ганта из отчета об анализе журнала регистрации не собиралась правильно: я рассчитывал рамки по реальному размеру рисунка. Когда перешел на холст 480 x 360, результат совпал с точностью до бита.
Почему не совпадает последний бит

Следующая загадка нашлась в одной из форм ERP. Координата рамки соответствовала 338 точкам из 360. Обычное деление в Python дает:
>>> 338 / 3600.9388888888888889
Платформа записывала 9.388888888888888e-1 — чуть меньше, с отличием в последнем разряде.
С дробью 19/360 происходило то же самое:
|
Расчет |
Результат |
|---|---|
|
Python |
|
|
Платформа |
|
Для изображения разница незаметна. Для совпадения с точностью до бита — критична.
Причина оказалась в преобразовании чисел. Платформа хранит десятичное число в виде блоков по девять цифр — то есть использует длинную арифметику с основанием миллиард.
Чтобы получить из него двоичное число с плавающей точкой, платформа:
-
Отбрасывает нулевые блоки по краям.
-
Собирает три старших блока: умножает накопленное значение на миллиард и прибавляет следующий блок. На каждом шаге происходит округление.
-
Делит результат на нужную степень миллиарда.
Из-за промежуточных округлений результат может отличаться от обычного деления исходных чисел. Так и “уезжал” последний бит.
Я повторил порядок операций платформы — и все рамки совпали.
Здесь важна последовательность: двоичное значение получается из уже сохраненного десятичного числа, а не напрямую из исходной дроби. Для точного совпадения пришлось воспроизвести весь путь преобразования.
Палитра на 30 цветов и серия с номером -1

Цвет серии в XML часто задан как auto. Но во внутренней записи вместо него стоит конкретный цвет. Откуда он берется?
В библиотеке диаграмм есть палитра из 30 цветов. Индекс цвета — остаток от деления номера серии на 30. Маркер выбирается похожим способом: остаток от деления на 3 определяет ромб, квадрат или круг.
Самое интересное происходит со сводной серией. Ее номер — -1.
При выборе цвета платформа рассматривает этот номер как беззнаковое 64-битное число. Значение -1 превращается в 2^64 - 1:
>>> (2**64 - 1) % 3015
В результате сводная серия получает цвет с индексом 15.
А при выборе маркера номер остается знаковым. Операция вычисления остатка в коде платформы дает для -1 значение -1. Такой вариант не предусмотрен, поэтому выбирается маркер по умолчанию — квадрат.
У диаграммы в реквизите формы есть своя таблица цветов. Кроме того, выбор зависит от флага типа диаграммы: график, ступенчатая, биржевая и другие “линейные” виды используют цвета иначе, чем круговая.
Значение этого флага я проверял уже на работающем процессе с помощью Frida: для круговой диаграммы получил 0, для графика — 1.
Как платформа раскладывает легенду
Самая сложная часть нашлась в формах.
Если у диаграммы реквизита формы есть серии, а легенда расположена у одного из краев, платформа при загрузке сама рассчитывает ее положение и размеры.
Правила оказались такими:
-
Каждой серии выделяется ячейка: у графика — 32 x 19 точек, у гистограммы — 19 x 19.
-
Сводная серия в расчете не участвует.
-
Текст серий и размеры поля на эту раскладку не влияют.
-
Легенда размещается по центру выбранного края; нечетная точка уходит вниз.
-
Область построения отступает от края с легендой на размер одной ячейки.
Эти правила я получил экспериментально. Загружал одну и ту же форму с одной, двумя, тремя и четырьмя сериями, каждый раз размещая легенду у разных краев. Сравнение результатов позволило восстановить алгоритм.
Обнаружилась и странность: заголовок с текстом 12313, расположенный у верхнего края, получил пустую рамку. Похоже, при загрузке платформа рассчитывает его положение так, будто текста нет. Но это пока только наблюдение по сохраненной записи — подтверждения в коде я не нашел.
Как я искал правила
Сначала я сравнивал образцы: вот XML, вот внутренняя запись — осталось найти закономерность.
На диаграммах этот подход быстро перестал работать. Образцов мало, полей десятки, а на результат влияют скрытые преобразования.
В итоге сложился такой порядок работы:
|
Шаг |
Инструмент |
Что удалось узнать |
|---|---|---|
|
Найти код сохранения диаграммы |
Дизассемблер |
Порядок полей и различия между версиями записи |
|
Найти таблицы и константы |
Ссылки на данные в коде |
Палитры, перечисления, варианты размещения легенды и заголовка |
|
Проверить значения во время работы |
Frida |
Значения флагов, которые трудно определить по одному коду |
|
Восстановить оставшиеся расчеты |
Серия загрузок в платформе |
Правила автоматической раскладки |
Основные правила я извлекал из кода. Там, где оставались пробелы, ставил отдельные эксперименты. А эталонные файлы использовал для итоговой проверки: совпало ли все до последнего бита.
Что осталось за рамками
Саму отрисовку Ганта — размещение интервалов, связей и текста — я не разбирал. В исследованных конфигурациях диаграммы пустые, поэтому для их точного восстановления эта часть не понадобилась.
Отрисовка заслуживает отдельного разбора. Если интересно — напишите.
А пока два вопроса:
-
Встречали форматы, где одно число хранится сразу в двух представлениях? Как проверяли точность?
-
Что было бы интересно разобрать следующим?
ссылка на оригинал статьи https://habr.com/ru/articles/1101884/