Odborný článok

Načítanie PDFium byte range pre vložené PDF v Delphi

PDFium Component dokáže otvoriť PDF, ktorý žije vnútri väčšieho buffera, priamo z rozsahu bajtov. Preťaženie LoadDocument(const Data: TBytes; Index, Count: Integer; Buffered: Boolean) adresuje okno na mieste, takže nie je potrebný žiadny predbežný Copy. Výmenou vás žiada pochopiť jedno pravidlo: keď je Buffered False, podkladové pole je požičané, nie skopírované

Toto je odlišný mechanizmus od prístupu riadeného callbackom opísaného v streamovaní veľkých PDF na požiadanie s PDFium VCL, ktorý PDFium odovzdá čítač FPDF_FILEACCESS a necháva ho ťahať bloky z disku podľa potreby. Ten je pre dokumenty príliš veľké na to, aby sa zmestili do RAM. Tento je pre dokumenty už v RAM, sediace na známom offsete vnútri niečoho iného. Tie dva sa dopĺňajú, a posledná sekcia vysvetľuje, ktorá situácia patrí ktorej

40 MB kópia, o ktorú nikto nežiadal

Scenár sa objavuje všade, kde PDF cestujú vnútri iných formátov. Mailový sklad drží telá správ a prílohy v jednom zázname. Archívny kontajner zreťazuje manifest, pár obrázkov a PDF. Vlastný drôtový protokol rámuje dokument za hlavičkou s prefixom dĺžky. V každom prípade skončíte s jedným veľkým TBytes a vedomosťou, že PDF začína na bajte 1 182 336 a beží 312 kilobajtov

Predtým, než existovalo preťaženie rozsahu bajtov, idiomatickou odpoveďou bolo Copy(Data, Index, Count), ktoré alokuje druhé pole a memcpy-uje okno doň. Potom tento výrez odovzdáte LoadDocument s Buffered = True, čo ho skopíruje znova do súkromného buffera komponenty. Dve kópie tých istých bajtov, jedna z nich čistá ceremónia, a pri veľkom skenovaní schránky opakovaná pre každú správu. Preťaženie rozsahu bajtov odstraňuje prvú kópiu bezpodmienečne a druhú voliteľne

Čo preťaženie rozsahu bajtov skutočne robí

Preťaženie je zámerne tenké: validuje, vypočíta jeden ukazovateľ, a deleguje na ukazovateľovú formu LoadDocument, cez ktorú sa už zlieva celá rodina. Index je od nuly, Count je dĺžka v bajtoch, a Buffered je predvolene True, presne tak ako pri ostatných preťaženiach. Jednoargumentové LoadDocument(const Data: TBytes; Buffered: Boolean) je teraz samo osebe iba volaním tohto s Index = 0 a Count = Length(Data), takže existuje jedna validačná cesta namiesto dvoch

Jeho volanie vyzerá ako kód, ktorý ste už písali, mínus výrez

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;

Prečo Index plus Count pretečie kontrolu hraníc?

Pretože Index aj CountInteger, a súčet dvoch veľkých kladných hodnôt Integer nie je nutne veľký kladný Integer. Toto je technické jadro preťaženia, a je to jediné miesto, kde prirodzene vyzerajúca kontrola je diera v bezpečnosti pamäte. Zjavná formulácia je nesprávna

// 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');

Prejdite si zlyhávajúci prípad. Zoberte Index = 2000000000 a Count = 2000000000. Ich skutočný súčet je štyri miliardy, ale v 32-bitovej znamienkovej aritmetike výsledok pretečie na presne mínus 294 967 296. Táto hodnota je pohodlne menšia než Length(Data), takže nesprávna kontrola prejde, @Data[Index] sa vezme ďaleko mimo pole, a PDFium dostane divoký ukazovateľ plus dvojgigabajtovú dĺžku. Čo nasleduje, je v dobrý deň porušenie prístupu a v zlý deň tiché parsovanie nesúvisiacej pamäte procesu

Správne poradie to opravuje tak, že nikdy nesčíta. Záporné hodnoty sa odmietnu skôr, než je čokoľvek indexované, takže @Data[Index] sa nikdy nevezme pod pole. Potom je Index ohraničený sám osebe voči Length(Data), čo garantuje, že Length(Data) - Index je nezáporný Integer. Až potom sa Count porovná voči tomuto zvyšku. Každá medziľahlá hodnota zostáva vnútri reprezentovateľného rozsahu, takže žiadna build konfigurácia nemôže zmeniť výsledok. Nedajte sa zlákať spoliehať sa ani na kontrolu pretečenia {$Q+} ako záchrannú sieť: release buildy bežne dodávajú s ňou vypnutou, a aj keď je zapnutá, premenili ste chybu bezpečnosti pamäte na EIntOverflow unikajúcu zo stredu validačnej rutiny. PDFium Component zaobchádza s nedôveryhodnou aritmetikou dĺžky rovnako, ako zaobchádza so zvyškom hranice, disciplína pokrytá šíršie v spevňovaní PDFium VCL ABI a bezpečnosti pamäte v Delphi

Prečo musí okno s nulovou dĺžkou odovzdať nil?

Pretože @Data[Index] nie je legálny výraz pre každý Index, ktorý validácia akceptuje. Index = Length(Data) s Count = 0 je dokonale správne formované prázdne okno na chvoste buffera, a prázdne TBytes dáva Index = 0 na poli, ktoré vôbec nemá prvok nula. Vzatie adresy v ktoromkoľvek prípade indexuje za koniec, alebo dereferencuje nil dynamické pole. Takže preťaženie sa vetví: Count = 0 vyprodukuje nil ukazovateľ, akékoľvek iné počítanie vyprodukuje @Data[Index]. Nil potom tečie do ukazovateľového preťaženia, ktorého vlastná poistka akceptuje nil ukazovateľ, keď je veľkosť nula, a načítanie skončí obyčajnou chybou „Cannot load PDF document“ namiesto porušenia prístupu. Volajúci, ktorý vypočítal nulabajtové okno z chybne formovaného kontajnera, dostane čistú, zachytiteľnú EPdfError ako pri akomkoľvek inom zlom vstupe

Požičané alebo skopírované: čo rozhoduje Buffered

Buffered volí kontrakt vlastníctva, a je to jediný parameter tu, ktorý má dôsledky presahujúce samotné volanie. S Buffered = True PDFium Component skopíruje vybrané okno, a iba okno, do svojho interného buffera pred načítaním. 40 MB kontajner sa nekopíruje; 312 KB PDF sa kopíruje. Akonáhle LoadDocument vráti riadenie, môžete kontajner okamžite uvoľniť, znovu použiť alebo prepísať, pretože komponenta naň už neodkazuje. Toto je predvolené nastavenie a správna voľba pre takmer všetok kód

Buffered = False odovzdá @Data[Index] priamo do FPDF_LoadMemDocument64, a PDFium si tento ukazovateľ ponechá počas celej životnosti dokumentu namiesto kopírovania bajtov. To robí načítanie bez alokácie, a robí to celé podkladové TBytes požičaným zdrojom. Musí zostať živé a nezmenené, kým nebeží UnloadDocument alebo kým Active nejde na False. Nie okno, celé pole: dynamické pole je počítané referenciami ako jednotka, a uvoľnenie poslednej referencie kdekoľvek vo vašom kóde uvoľní pamäť, ktorú PDFium ešte číta. Nastavenie Length na ňom je rovnako fatálne, pretože realokácia môže presunúť blok. Uveďte toto vo vlastnej dokumentácii API všade, kde takéto načítanie sprístupňujete, v tom istom duchu ako akúkoľvek inú hranicu požičaj-verzus-vlastni v Pascal kóde; režim zlyhania je identický s hazardami aliasovania opísanými v úniku FillChar a výsledného reťazca v Delphi, kde buffer vyzerá vlastnený, ale nie je

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;

Keď je okno rozsahu bajtov nesprávny nástroj

Buďte úprimní ohľadom hranice. Preťaženie rozsahu bajtov predpokladá, že kontajner je už úplne v pamäti, a Count je Integer, takže jedno okno nemôže presiahnuť dva gigabajty. Ak je kontajner 6 GB archív na disku, alebo prichádza cez socket, ktorý nemôžete pretočiť späť, toto preťaženie vám nemôže pomôcť a čítanie celej veci do TBytes len na adresovanie okna vnútri neho poráža celý zmysel. Presne tam patrí cesta FPDF_FILEACCESS, a článok o streamovaní na požiadanie ukazuje, ako sprístupniť pohľad na súbor posunutý o offset ako vlastný zdroj dokumentu. Rovnako, ak vložené bajty potrebujú transformáciu skôr, než ich PDFium uvidí, dekompresiu, dešifrovanie, krok rozbalenia, potom je skutočná kópia nevyhnutná a Buffered = True na transformovanom poli je úprimná odpoveď. Okno rozsahu bajtov sa oplatí presne v jednom tvare: súvislé, nezmenené PDF bajty, už rezidentné, na známom offsete

Ak toto zvažujete pre viewer, náhľadový panel alebo dávkovú intake pipeline, preťaženie rozsahu bajtov a streamovací loader sú dve zo stratégií načítania, ktoré PDFium Component dodáva popri načítaní zo súboru, streamu a surového ukazovateľa. Úplný povrch API, licencovanie a podpora verzií Delphi a C++Builder sú zdokumentované na stránke produktu PDFium Component