Tehnički članak

PDFium učitavanje raspona bajtova za ugrađene PDF-ove u Delphiju

PDFium Component može otvoriti PDF koji živi unutar većeg međuspremnika izravno iz raspona bajtova. Preopterećenje LoadDocument(const Data: TBytes; Index, Count: Integer; Buffered: Boolean) adresira prozor na mjestu, tako da prethodni Copy nije potreban. Zauzvrat traži da razumijete jedno pravilo: kad je Buffered False, podložni niz je posuđen, ne kopiran

Ovo je drugačiji mehanizam od pristupa vođenog povratnim pozivom opisanog u streamingu velikih PDF-ova na zahtjev s PDFium VCL, koji PDFium-u predaje čitač FPDF_FILEACCESS i pušta ga da povlači blokove s diska kako mu trebaju. To je za dokumente prevelike da stanu u RAM. Ovo je za dokumente koji su već u RAM-u, na poznatom offsetu unutar nečeg drugog. Dva su komplementi, a posljednji odjeljak objašnjava koja situacija pripada kojem

Kopija od 40 MB koju nitko nije tražio

Scenarij se pojavljuje gdje god PDF-ovi putuju unutar drugih formata. Pohrana pošte drži tijela poruka i privitke u jednom zapisu. Kontejner arhive spaja manifest, nekoliko slika i PDF. Prilagođeni žični protokol uokviruje dokument iza zaglavlja s prefiksom duljine. U svakom slučaju završite s jednim velikim TBytes i znanjem da PDF počinje na bajtu 1.182.336 i traje 312 kilobajta

Prije nego je preopterećenje raspona bajtova postojalo, idiomatski odgovor bio je Copy(Data, Index, Count), koji alocira drugi niz i memcpy-ja prozor u njega. Zatim taj isječak predajete LoadDocument s Buffered = True, koji ga ponovno kopira u privatni međuspremnik komponente. Dvije kopije istih bajtova, jedna od njih čista ceremonija, i na velikom skeniranju poštanskog sandučića ponovljena za svaku poruku. Preopterećenje raspona bajtova uklanja prvu kopiju bezuvjetno i drugu neobvezno

Usporedni dijagram koji prikazuje staru putanju dvostrukog kopiranja za ugrađeni PDF unutar spremnika u Delphiju nasuprot PDFium Component byte-range LoadDocument pretovaru koji adresira prozor na mjestu
Bez overloada isti 312 KB prozor dvaput je memcpyiran; byte-range poziv ga adresira na mjestu, a Buffered bira kopira li se uopće išta

Što preopterećenje raspona bajtova zapravo radi

Preopterećenje je namjerno tanko: provjerava, izračunava jedan pokazivač, i delegira na oblik pokazivača LoadDocument kroz koji već prolazi cijela obitelj. Index je baziran na nuli, Count je duljina u bajtovima, a Buffered zadano je True točno kao i na ostalim preopterećenjima. Jednoargumentni LoadDocument(const Data: TBytes; Buffered: Boolean) sam je sad samo poziv ovome s Index = 0 i Count = Length(Data), tako da postoji jedan put provjere umjesto dva

Pozivanje izgleda poput koda koji ste već pisali, minus isječak

var
  Frame: TBytes;          // cijeli zapis kontejnera, deseci megabajta
  Offset, Size: Integer;
begin
  Frame := LoadContainerRecord('mailbox.dat');
  LocateEmbeddedPdf(Frame, Offset, Size);   // vaš parser kontejnera

  // Ovdje nema Copy(Frame, Offset, Size) - prozor se adresira na mjestu
  Pdf.LoadDocument(Frame, Offset, Size, True);
  try
    RenderPreview(Pdf);
  finally
    Pdf.UnloadDocument;
  end;
end;

Zašto Index plus Count prelijeva provjeru granica?

Zato što su Index i Count oba Integer, a zbroj dvije velike pozitivne Integer vrijednosti nije nužno velik pozitivan Integer. Ovo je tehnička srž preopterećenja, i jedino mjesto gdje prirodno izgledajuća provjera je rupa u sigurnosti memorije. Očita formulacija je pogrešna

// POGREŠNO: Index + Count izračunava se u Integer i može se omotati u negativno
if Index + Count <= Length(Data) then
  DataPtr := @Data[Index];

// ISPRAVNO: prvo odbaci predznake, zatim ograniči svaki član zasebno,
// jedina aritmetika je oduzimanje koje se ne može omotati
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');

Provedite kvarni slučaj kroz to. Uzmite Index = 2000000000 i Count = 2000000000. Njihov je stvarni zbroj četiri milijarde, ali u 32-bitnoj potpisanoj aritmetici rezultat se omota u točno minus 294.967.296. Ta je vrijednost udobno manja od Length(Data), pa pogrešna provjera prolazi, @Data[Index] se uzima daleko izvan niza, a PDFium-u se predaje divlji pokazivač plus duljina od dva gigabajta. Ono što slijedi je narušavanje pristupa u dobrom danu i tiho raščlanjivanje nepovezane memorije procesa u lošem

Ispravan redoslijed ovo popravlja nikad ne zbrajajući. Negativne vrijednosti odbijaju se prije nego se bilo što indeksira, tako da se @Data[Index] nikad ne može uzeti ispod niza. Zatim se Index ograničava samostalno protiv Length(Data), što jamči da je Length(Data) - Index nenegativan Integer. Tek se onda Count uspoređuje s tim ostatkom. Svaka posredna vrijednost ostaje unutar predstavljivog raspona, tako da nijedna konfiguracija builda ne može promijeniti ishod. Ne dajte se namamiti na oslanjanje na provjeru prelijevanja {$Q+} kao mrežu sigurnosti niti: release buildovi rutinski se isporučuju s njom isključenom, a čak i kad je uključena, pretvorili ste grešku sigurnosti memorije u EIntOverflow koji izlazi iz sredine rutine provjere. PDFium Component tretira nepouzdanu aritmetiku duljine na isti način na koji tretira ostatak granice, disciplina pokrivena šire u jačanju PDFium VCL ABI-ja i sigurnosti memorije u Delphiju

Dijagram toka koji suprotstavlja preplavljenu provjeru granica Index plus Count redoslijedom provjere samo oduzimanjem koji čuva PDFium byte-range učitavanje memorijski sigurnim u Delphiju
Prvo zbrajanje prelijeva preko MaxInt i poništava čuvara; odbijanje predznaka i ograničavanje članova prije jednog oduzimanja bez preljeva svaki međukorak drži predstavljivim

Zašto prozor nulte duljine mora proslijediti nil?

Zato što @Data[Index] nije legalan izraz za svaki Index koji provjera prihvaća. Index = Length(Data) s Count = 0 savršeno je dobro oblikovan prazan prozor na repu međuspremnika, a prazan TBytes daje Index = 0 na nizu koji uopće nema element nula. Uzimanje adrese u bilo kojem slučaju indeksira iza kraja, ili dereferencira nil dinamički niz. Pa se preopterećenje grana: Count = 0 daje nil pokazivač, svaki drugi count daje @Data[Index]. Nil zatim teče u preopterećenje pokazivača, čija vlastita zaštita prihvaća nil pokazivač kad je veličina nula, a učitavanje završava u običnoj grešci "Cannot load PDF document" umjesto narušavanja pristupa. Pozivatelj koji je izračunao prozor od nula bajtova iz neispravnog kontejnera dobiva čist, uhvatljiv EPdfError poput svakog drugog lošeg ulaza

Posuđeno ili kopirano: što Buffered odlučuje

Buffered bira ugovor vlasništva, i jedini je parametar ovdje s posljedicama izvan poziva. S Buffered = True, PDFium Component kopira odabrani prozor, i samo prozor, u svoj interni međuspremnik prije učitavanja. Kontejner od 40 MB se ne kopira; PDF od 312 KB se kopira. Čim se LoadDocument vrati, možete odmah osloboditi, ponovno koristiti ili prepisati kontejner, jer se komponenta više ne referencira na njega. Ovo je zadano i ispravan izbor za gotovo sav kod

Buffered = False prosljeđuje @Data[Index] izravno u FPDF_LoadMemDocument64, a PDFium zadržava taj pokazivač tijekom cijelog života dokumenta umjesto kopiranja bajtova. To čini učitavanje bez alokacije, i čini cijeli podložni TBytes posuđenim resursom. Mora ostati živ i nepromijenjen dok se UnloadDocument ne pokrene ili Active ne postane False. Ne prozor, cijeli niz: dinamički niz broji reference kao jedinica, a puštanje posljednje reference bilo gdje u vašem kodu oslobađa memoriju koju PDFium još čita. Postavljanje Length na njemu jednako je kobno, jer realokacija može premjestiti blok. Navedite ovo u vlastitoj API dokumentaciji gdje god izlažete takvo učitavanje, u istom duhu kao i svaka druga granica posuđivanja-nasuprot-posjedovanja u Pascal kodu; način neuspjeha identičan je opasnostima aliasinga opisanim u FillChar i curenju result stringa u Delphiju, gdje međuspremnik izgleda posjedovan, a nije

type
  TFrameSession = class
  private
    FFrame: TBytes;   // posjeduje podložnu pohranu dokle god je FPdf učitan
    FPdf: TPdf;
  public
    procedure OpenEmbedded(Offset, Size: Integer);
    destructor Destroy; override;
  end;

procedure TFrameSession.OpenEmbedded(Offset, Size: Integer);
begin
  // Buffered = False: FFrame mora nadživjeti učitani dokument
  FPdf.LoadDocument(FFrame, Offset, Size, False);
end;

destructor TFrameSession.Destroy;
begin
  FPdf.UnloadDocument;   // prvo oslobodi posudbu
  FFrame := nil;         // tek sad pohrana smije otići
  inherited;
end;

Kad je prozor raspona bajtova pogrešan alat

Budite iskreni oko granice. Preopterećenje raspona bajtova pretpostavlja da je kontejner već posve u memoriji, a Count je Integer, pa jedan prozor ne može premašiti dva gigabajta. Ako je kontejner arhiva od 6 GB na disku, ili stiže preko socketa koji ne možete premotati, ovo preopterećenje vam ne može pomoći, a čitanje cijele stvari u TBytes samo da biste adresirali prozor unutar nje poražava poantu. To je upravo gdje pripada put FPDF_FILEACCESS, a članak o streamingu na zahtjev pokazuje kako izložiti pomaknuti prikaz datoteke kao prilagođeni izvor dokumenta. Jednako tako, ako ugrađeni bajtovi trebaju transformaciju prije nego ih PDFium vidi, dekompresiju, dešifriranje, korak raspakiranja, onda je prava kopija neizbježna, a Buffered = True na transformiranom nizu je iskren odgovor. Prozor raspona bajtova isplati se u točno jednom obliku: susjedni, nepromijenjeni PDF bajtovi, već rezidentni, na poznatom offsetu

Ako ovo procjenjujete za preglednik, panel pregleda ili pipeline paketnog unosa, preopterećenje raspona bajtova i streaming učitavač dvije su od strategija učitavanja koje PDFium Component isporučuje uz učitavanje iz datoteke, streama i sirovog pokazivača. Puna površina API-ja, licenciranje i podrška verzija Delphi i C++Builder dokumentirani su na stranici PDFium Component proizvoda

Dijagram vremenske crte dva Buffered načina pri učitavanju ugrađenog PDF-a iz bajtovskog raspona u Delphiju: kopiranje samo PDF prozora i trenutačno oslobađanje spremnika nasuprot posuđivanju cijelog podložnog TBytes dok se UnloadDocument ne izvrši
Buffered = True samo prozor kopira pa je spremnik jednokratan, dok Buffered = False cijelo pozadinsko polje posuđeno ostavlja do istovara