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

Завантаження PDFium за байтовим діапазоном для вбудованих PDF у Delphi

PDFium Component може відкрити PDF, що живе всередині більшого буфера, прямо з байтового діапазону. Перевантаження LoadDocument(const Data: TBytes; Index, Count: Integer; Buffered: Boolean) адресує вікно на місці, тож попередній Copy не потрібен. Натомість воно просить вас зрозуміти одне правило: коли BufferedFalse, базовий масив запозичений, не скопійований

Це інший механізм, ніж підхід на основі зворотного виклику, описаний у потоковій передачі великих PDF на вимогу з PDFium VCL, що надає PDFium читач FPDF_FILEACCESS і дозволяє йому витягувати блоки з диска за потреби. Той підхід — для документів, надто великих, щоб уміститись у RAM. Цей — для документів, вже в RAM, розташованих на відомому зсуві всередині чогось іншого. Обидва — доповнення, і останній розділ пояснює, яка ситуація належить якому

Копія на 40 МБ, яку ніхто не просив

Сценарій з'являється всюди, де PDF подорожують всередині інших форматів. Поштове сховище тримає тіла повідомлень і вкладення в одному записі. Контейнер архіву конкатенує маніфест, кілька зображень і PDF. Власний протокол проводки обрамлює документ за заголовком з довжиною спереду. У кожному випадку ви опиняєтесь з одним великим TBytes, знаючи, що PDF починається на байті 1 182 336 і триває 312 кілобайтів

До того, як з'явилось перевантаження байтового діапазону, ідіоматичною відповіддю було Copy(Data, Index, Count), що виділяє другий масив і виконує memcpy вікна в нього. Потім ви передаєте цей зріз у LoadDocument з Buffered = True, що копіює його знову в приватний буфер компонента. Дві копії тих самих байтів, одна з них — чиста церемонія, і на скані великої поштової скриньки повторена для кожного повідомлення. Перевантаження байтового діапазону усуває першу копію безумовно, а другу — опційно

Що насправді робить перевантаження байтового діапазону

Перевантаження навмисно тонке: воно перевіряє, обчислює один покажчик і делегує формі LoadDocument з покажчиком, через яку вже проходить уся родина. Index з відліком від нуля, Count — довжина в байтах, а Buffered за замовчуванням True точнісінько як на інших перевантаженнях. Однoаргументний LoadDocument(const Data: TBytes; Buffered: Boolean) сам тепер — просто виклик цього з Index = 0 та Count = Length(Data), тож існує один шлях валідації замість двох

Виклик виглядає як код, що ви вже писали, мінус зріз

var
  Frame: TBytes;          // whole container record, tens of megabytes
  Offset, Size: Integer;
begin
  Frame := LoadContainerRecord('mailbox.dat');
  LocateEmbeddedPdf(Frame, Offset, Size);   // your container parser

  // No Copy(Frame, Offset, Size) here - the window is addressed in place
  Pdf.LoadDocument(Frame, Offset, Size, True);
  try
    RenderPreview(Pdf);
  finally
    Pdf.UnloadDocument;
  end;
end;

Чому Index плюс Count переповнює перевірку меж?

Тому що Index і Count обидва — Integer, а сума двох великих додатних значень Integer не обов'язково велике додатне Integer. Це технічне ядро перевантаження, і це те єдине місце, де природно виглядаюча перевірка — діра безпеки пам'яті. Очевидне формулювання неправильне

// WRONG: Index + Count is evaluated in Integer and can wrap negative
if Index + Count <= Length(Data) then
  DataPtr := @Data[Index];

// RIGHT: reject signs first, then bound each term separately,
// with the only arithmetic done as a subtraction that cannot wrap
Check(Index >= 0,  'PDF byte range index cannot be negative');
Check(Count >= 0,  'PDF byte range count cannot be negative');
Check(Index <= Length(Data), 'PDF byte range index exceeds data length');
Check(Count <= Length(Data) - Index, 'PDF byte range exceeds data length');

Пропрацюйте відмовний випадок. Візьміть Index = 2000000000 та Count = 2000000000. Їхня справжня сума — чотири мільярди, але в 32-бітній арифметиці зі знаком результат обгортається рівно до мінус 294 967 296. Це значення затишно менше за Length(Data), тож неправильна перевірка проходить, @Data[Index] береться далеко за межами масиву, і PDFium отримує дикий покажчик плюс довжину два гігабайти. Що слідує далі — порушення доступу у гарний день і тихий розбір непов'язаної пам'яті процесу в поганий

Правильний порядок виправляє це, ніколи не додаючи. Від'ємні значення відхиляються до того, як щось індексується, тож @Data[Index] ніколи не може бути взятий нижче масиву. Потім Index обмежується сам по собі проти Length(Data), що гарантує, що Length(Data) - Index — невід'ємний Integer. Лише тоді Count порівнюється з цим залишком. Кожне проміжне значення залишається в межах представного діапазону, тож жодна конфігурація збірки не може змінити результат. Не піддавайтесь спокусі покладатись на перевірку переповнення {$Q+} як на страховку: релізні збірки регулярно постачаються з нею вимкненою, і навіть коли вона увімкнена, ви перетворили діру безпеки пам'яті на EIntOverflow, що витікає з середини процедури валідації. PDFium Component трактує недовірену арифметику довжини так само, як решту межі, дисципліна, ширше охоплена в зміцненні ABI PDFium VCL та безпеки пам'яті в Delphi

Чому вікно нульової довжини має передавати nil?

Тому що @Data[Index] — не легальний вираз для кожного Index, який приймає валідація. Index = Length(Data) з Count = 0 — цілком добре сформоване порожнє вікно на хвості буфера, а порожній TBytes дає Index = 0 на масиві, що взагалі не має елемента нуль. Взяття адреси в будь-якому випадку індексує за кінець, чи розіменовує nil-динамічний масив. Тож перевантаження розгалужується: Count = 0 дає nil-покажчик, будь-яка інша кількість дає @Data[Index]. Nil потім тече в перевантаження з покажчиком, чий власний захист приймає nil-покажчик, коли розмір нуль, і завантаження завершується звичайною помилкою "Cannot load PDF document", а не порушенням доступу. Викликач, що обчислив нульобайтове вікно з деформованого контейнера, отримує чисту, перехоплювану EPdfError, як і будь-які інші погані вхідні дані

Запозичений чи скопійований: що вирішує Buffered

Buffered обирає контракт володіння, і це єдиний параметр тут з наслідками поза викликом. З Buffered = True PDFium Component копіює обране вікно, і лише вікно, у свій внутрішній буфер перед завантаженням. 40 МБ контейнер не копіюється; 312 КБ PDF копіюється. Щойно LoadDocument повертається, ви можете звільнити, повторно використати чи перезаписати контейнер негайно, бо компонент більше на нього не посилається. Це типове й правильне обрання для майже всього коду

Buffered = False передає @Data[Index] прямо в FPDF_LoadMemDocument64, і PDFium тримає цей покажчик протягом усього життя документа замість копіювання байтів. Це робить завантаження вільним від виділень, і це робить весь базовий TBytes запозиченим ресурсом. Він мусить залишатись живим і незмінним, доки не виконається UnloadDocument чи Active не стане False. Не вікно, весь масив: динамічний масив підрахований за посиланнями як одиниця, і дозвіл останньому посиланню зникнути будь-де у вашому коді звільняє пам'ять, яку PDFium все ще читає. Встановлення Length на ньому так само фатальне, бо переалокація може перемістити блок. Зафіксуйте це у власній документації API всюди, де ви оприлюднюєте таке завантаження, у тому ж дусі, що й будь-яка інша межа запозичення проти володіння в коді Pascal; режим відмови ідентичний небезпекам аліасингу, описаним у витоку FillChar та рядка результату в Delphi, де буфер виглядає як власний, а насправді ні

type
  TFrameSession = class
  private
    FFrame: TBytes;   // owns the backing storage for as long as FPdf is loaded
    FPdf: TPdf;
  public
    procedure OpenEmbedded(Offset, Size: Integer);
    destructor Destroy; override;
  end;

procedure TFrameSession.OpenEmbedded(Offset, Size: Integer);
begin
  // Buffered = False: FFrame must outlive the loaded document
  FPdf.LoadDocument(FFrame, Offset, Size, False);
end;

destructor TFrameSession.Destroy;
begin
  FPdf.UnloadDocument;   // release the borrow first
  FFrame := nil;         // only now may the storage go
  inherited;
end;

Коли вікно байтового діапазону — неправильний інструмент

Будьте чесними щодо межі. Перевантаження байтового діапазону припускає, що контейнер уже повністю в пам'яті, а CountInteger, тож одне вікно не може перевищувати два гігабайти. Якщо контейнер — 6 ГБ архів на диску, чи прибуває через сокет, який ви не можете перемотати, це перевантаження вам не допоможе, і читання всього цього в TBytes лише щоб адресувати вікно всередині нього зводить нанівець сенс. Саме тут місце шляху FPDF_FILEACCESS, і стаття про потокову передачу на вимогу показує, як представити зсунуте на зсув вид файлу як власне джерело документа. Однаково, якщо вбудовані байти потребують трансформації перед тим, як PDFium їх побачить, — декомпресії, дешифрування, розгортання, — тоді справжня копія неминуча, і Buffered = True над трансформованим масивом — чесна відповідь. Вікно байтового діапазону окупається рівно в одній формі: суцільні, незмінні байти PDF, вже резидентні, на відомому зсуві

Якщо ви оцінюєте це для переглядача, панелі попереднього перегляду чи конвеєра пакетного прийому, перевантаження байтового діапазону та потоковий завантажувач — дві зі стратегій завантаження, які PDFium Component постачає поряд із завантаженнями з файлу, потоку та сирого покажчика. Повна поверхня API, ліцензування та підтримка версій Delphi й C++Builder задокументовані на сторінці продукту PDFium Component