Технічна стаття

Діагностика мовчазних збоїв заглушок у Pascal PDF-бібліотеці

Коли бібліотека Delphi вирощує конфігурацію збірки без візуального фреймворка, саме в замінних класах живуть вади. Не в платформі, не в компіляторі: у замінах. PDFlibPas має графічний шар, що дає еквіваленти бітової карти, канви, шрифта, метафайла та принтера для збірок без VCL, і перенесення на Free Pascal виявило кожен режим збою, який заміна може мати. Вони акуратно сортуються за вартістю діагностики, і цей порядок протилежний тому, що навіює інтуїція

Заміну, що викидає виняток, дешево знайти; виняток називає метод. Заміна, що повертає порожні дані, дорога, бо збій проявляється за кілька шарів від своєї причини. Заміна, що повертає успіх, — найгірша з усіх, бо код повернення чинний, код помилки нуль, жодного винятку не викинуто, і єдиний доказ того, що щось пішло не так, — у байтах, які вийшли

Три форми мовчазного збою заглушки в бібліотеці PDF на Pascal, ранжовані за вартістю діагностики: від заглушки з винятком до успіху над порожнім результатом
Заміну з винятком діагностувати дешево, порожні дані дорого, а успішне повернення над порожнім артефактом — найважче знайти

Форма три: чинний ідентифікатор зображення над порожнім XObject

Векторний конвертер метафайлів був порожнім тілом процедури в конфігурації без VCL. Усе над ним продовжувало працювати. Точки входу імпорту EMF і точка входу захоплення канви виконувалися до кінця й повертали законний ідентифікатор зображення, який викликач тоді клав на сторінку. У файл потрапляв form XObject із довжиною вмісту нуль. Сторінка рендерилася білою

Ніщо не повідомляло проблему, і це включає власну демонстраційну програму бібліотеки для цієї функції, яка малювала порожню сторінку й не помічала цього. Не було значення повернення зі збоєм, яке можна перевірити, бо послідовність викликів справді вся була успішною; єдиним неправильним був розмір створеного потоку. Діагностика цього класу вад означає інше питання: не «чи виклик зазнав невдачі», а «чи артефакт правдоподібний». Form XObject довжини нуль, зображення з нульовою кількістю пікселів, сторінка з нульовою кількістю байтів вмісту — це твердження, які ловлять її

Виправлення має дві половини, і другу легко забути. Перше: зробити порожню реалізацію такою, що викидає виняток, тож у збою взагалі з'явиться канал. Друге: перетворити той виняток на нульовий результат у фабриці зображень і додати перевірки на нуль у двох місцях, що споживають ідентифікатор зображення, бо інакше «чиста відмова» перетворюється прямо на порушення доступу, коли дерево сторінок розіменовує ніщо. Заглушка, що викидає, — покращення лише тоді, коли викликачі були готові до відмови, яку вони досі не мали змоги отримувати

Форма два: порожні дані за три шари від падіння

Заміна канви метафайла не заповнювала свої фізичні розміри. Те значення ділиться в геометричне обчислення сторінки, тож обчислення давало нуль, і обчислення обвідної рамки ділило на нуль. Голий обробник винятків проковтнув це, фабрика зображень повернула нульовий результат, а порушення доступу нарешті сталося в дереві сторінок, коли нуль використали. Три шари між причиною та симптомом, з обробником винятків посередині, що стирає докази

Ланцюг збою порожньої заміни канви метафайла в PDFlibPas, що доходить до порушення доступу за три шари після ділення на нуль
Порожній розмір зводить геометричне обчислення до нуля, голий обробник стирає виняток, а нульовий ідентифікатор валить дерево сторінок

Той самий модуль мав ще два випадки цієї схеми. Клас шрифта мав порожні тіла Assign та конструктора, що важить більше, ніж виглядає, бо властивість шрифта канви лише для читання: присвоєння в неї — єдиний спосіб доставити шрифт, тож порожня реалізація робить вибір шрифта мовчки нечинним, і текст виходить тим, яким був типовий. А значення пікселів на дюйм, рівне нулю, робило кожного викликача, що розміряє канву за метриками шрифта, творцем канви нуль на нуль, що дає білу сторінку та успішне повернення

// Схема, яку шукати в модулі замін: метод, що не викидає
// винятків і нічого не робить. Обидва компілюються, і обидва
// дають «успіх» без жодного результату
procedure TMetafileCanvasStandIn.Create(...);
begin
  // без виклику inherited, без ініціалізації полів
end;

function TBitmapStandIn.LoadFromStream(Stream: TStream): Boolean;
begin
  Result := True;    // а бітова карта досі порожня
end;

Широка структура, що лишає лише перший символ

Це взагалі не проблема замін, але вона належить до того самого каталогу, бо симптом так само далекий від причини. Структуру переліку принтерів оголосили з усіма дванадцятьма її рядковими членами як вказівниками на однобайтові символи, тоді як функція, що її заповнює, — широкосимвольний варіант API переліку

Розміри вказівників ідентичні, тож розкладка структура правильна, і нічого не падає. Натомість читання рядка UTF-16 як однобайтового рядка зупиняється на першому нульовому байті, яким для будь-якого ASCII-імені принтера є старша половина другого символу. Кожне ім'я принтера поверталося рівно з одним символом. Нижче за течією валідація імен зазнавала невдачі, створення принтера зазнавало невдачі, і друк зазнавав невдачі для кожного справжнього принтера на машині, і жоден із тих симптомів не вказує на оголошення структури

Ім'я принтера UTF-16, обрізане до одного символу, коли широка структура Win32 оголошена з членами PAnsiChar замість PWideChar
Однобайтові члени читають ім'я UTF-16 лише до першого нульового байта, тож кожне ім'я принтера повертається рівно з одним символом
// Неправильно: правильний розмір, неправильний тип елемента.
// Ні помилки компіляції, ні падіння, кожен рядок обрізаний
// до одного символу
type
  TPrinterInfo2Wrong = record
    pServerName: PAnsiChar;
    pPrinterName: PAnsiChar;
    // ... і ще десять
  end;

// Правильно: структура *W має всюди широкі члени
type
  TPrinterInfo2W = record
    pServerName: PWideChar;
    pPrinterName: PWideChar;
    // ... і ще десять
  end;

Правило, що звідси випливає, механічне і варте застосування без роздумів: для будь-якої структури Win32, чия назва закінчується на W, перевіряйте, що кожен рядковий член — широка варіанта, поле за полем. Змішування світів ANSI та широких не дає ані діагностики компілятора, ані падіння — лише мовчазне обрізання, і те саме стосується зворотно варіантів ANSI

Голий обробник винятків — справжній супротивник

Кожне з цих розслідувань сповільнювала та сама конструкція: обробник, що ловить усе і перетворює це на хибне значення повернення. Це розумна річ навколо декодера зображень, бо зіпсоване зображення не має валити завдання документа. Це також інструмент для вилучення тієї єдиної інформації, яка вам потрібна

Практична відповідь — зробити обробник тимчасово гучним. Вивантаження класу винятку, повідомлення та трасування зсередини голого обробника, під умовною компіляцією налагодження, перетворює незрозуміле нульове повернення на іменований виняток із місцем. У двох із трьох випадків вище той один крок завершив розслідування, бо винятком було ділення на нуль або порушення доступу в методі заміни, чия назва говорила все

Перелік перевірок для прийняття шляху замін

Чотири пункти в порядку їхньої віддачі. Перед викликом у замінний клас прочитайте методи, які збираєтеся використати, і пересвідчіться, що кожен має справжнє тіло; порожнє тіло — не деталь реалізації, а відсутня можливість. Надавайте перевагу замінам, що викидають винятки, над замінами, що повертають нейтральні значення, і поєднуйте це з перевірками на нуль у місцях, де фабрика тепер може законно повернути ніщо. Перевіряйте можливість оглядом артефакта, а не коду повернення, бо весь режим збою тут — чистий код повернення над порожнім артефактом; побайтовий розбір того, що документ справді містить, — найшвидший спосіб це побачити, і стаття про аудит розміру файла охоплює той інструментарій. І коли можливість не має життєздатної заміни, спрямовуйте уражені зразки на шлях, що працює, і скажіть чому в коментарі, замість того щоб лишати демонстрацію, яка мовчки дає порожній результат

Ширше спостереження стосується далеко за межами однієї бібліотеки. Будь-яка кодова база з умовною другою реалізацією, шаром моків, режимом без інтерфейсу, платформною обгорткою, відкрита до форми три. Причина, чому вона так добре ховається: кожна перевірка якості, на яку команда звикла покладатися, — коди повернення, коди помилок, винятки, статуси виходу, — це канал статусу, а форма три тримає всі їх чистими. Лише результат видає її. Це також логіка перевірки артефактів, а не статусів під час обробки недовірених даних, описана в статті про розбір недовірених PDF, і логіка порівняння відрендереного результату між рушіями замість довіри одному, описана в багаторушійному рендерингу

PDFlibPas — це власна бібліотека PDF на Object Pascal для Delphi, C++Builder і Free Pascal, і саме її конфігурація без VCL робить можливими безінтерфейсні та міжінструментарні збірки; поточне охоплення конфігурацій наведене на сторінці продукту losLab PDF Developer Library