Articol tehnic

Încărcare interval de octeți PDFium pentru PDF-uri încorporate în Delphi

PDFium Component poate deschide un PDF care trăiește în interiorul unui buffer mai mare direct dintr-un interval de octeți. Supraîncărcarea LoadDocument(const Data: TBytes; Index, Count: Integer; Buffered: Boolean) adresează o fereastră pe loc, așa că nu este necesar niciun Copy preliminar. În schimb vă cere să înțelegeți o regulă: când Buffered este False, array-ul suport este împrumutat, nu copiat

Acesta este un mecanism diferit de abordarea condusă de callback descrisă în streaming-ul PDF-urilor mari la cerere cu PDFium VCL, care predă lui PDFium un cititor FPDF_FILEACCESS și îl lasă să tragă blocuri de pe disc pe măsură ce are nevoie. Acela este pentru documente prea mari pentru a încăpea în RAM. Acesta este pentru documente deja în RAM, aflate la un offset cunoscut în interiorul altceva. Cele două sunt complementare, iar ultima secțiune explică ce situație aparține cărei metode

Copia de 40 MB pe care nimeni nu a cerut-o

Scenariul apare oriunde PDF-urile călătoresc în interiorul altor formate. Un depozit de mail păstrează corpurile mesajelor și atașamentele într-o singură înregistrare. Un container de arhivă concatenează un manifest, câteva imagini și un PDF. Un protocol de rețea personalizat încadrează un document în spatele unui antet cu prefix de lungime. În fiecare caz ajungeți să dețineți un TBytes mare și să știți că PDF-ul începe la octetul 1 182 336 și se întinde pe 312 kilobytes

Înainte ca supraîncărcarea de interval de octeți să existe, răspunsul idiomatic era Copy(Data, Index, Count), care alocă un al doilea array și copiază memoria ferestrei în el. Apoi predați acea felie lui LoadDocument cu Buffered = True, care o copiază din nou în buffer-ul privat al componentei. Două copii ale acelorași octeți, una dintre ele pură ceremonie, și pe o scanare de căsuță poștală mare repetată pentru fiecare mesaj. Supraîncărcarea de interval de octeți elimină necondiționat prima copie și opțional pe a doua

Ce face de fapt supraîncărcarea de interval de octeți

Supraîncărcarea este subțire prin design: validează, calculează un singur pointer și delegă la forma cu pointer a lui LoadDocument prin care întreaga familie trece deja. Index este bazat pe zero, Count este o lungime de octeți, iar Buffered este implicit True exact cum este pe celelalte supraîncărcări. Supraîncărcarea cu un singur argument LoadDocument(const Data: TBytes; Buffered: Boolean) este ea însăși acum doar un apel spre aceasta cu Index = 0 și Count = Length(Data), așa că există o singură cale de validare, nu două

Apelarea ei arată ca și codul pe care îl scriați deja, minus felia

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;

De ce depășește Index plus Count verificarea de limite?

Pentru că Index și Count sunt ambele Integer, iar suma a două valori mari pozitive Integer nu este neapărat un Integer mare pozitiv. Acesta este nucleul tehnic al supraîncărcării, și este singurul loc unde o verificare care arată natural este o gaură de siguranță a memoriei. Formularea evidentă este greșită

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

Parcurgeți cazul de eșec. Luați Index = 2000000000 și Count = 2000000000. Suma lor reală este patru miliarde, dar în aritmetica semnată pe 32 de biți, rezultatul se învârtește la exact minus 294 967 296. Acea valoare este confortabil mai mică decât Length(Data), așa că verificarea greșită trece, @Data[Index] este luat mult în afara array-ului, iar PDFium primește un pointer sălbatic plus o lungime de doi gigabyte. Ce urmează este o încălcare de acces într-o zi bună și analiza tacită a memoriei de proces fără legătură într-una rea

Ordinea corectă rezolvă asta nemaiadunând niciodată. Valorile negative sunt respinse înainte ca ceva să fie indexat, așa că @Data[Index] nu poate fi niciodată luat sub array. Apoi Index este mărginit de unul singur față de Length(Data), ceea ce garantează că Length(Data) - Index este un Integer nenegativ. Doar atunci Count este comparat cu acel rest. Fiecare valoare intermediară rămâne în intervalul reprezentabil, așa că nicio configurație de build nu poate schimba rezultatul. Nu vă lăsați tentați să vă bazați nici pe verificarea de overflow {$Q+} ca plasă de siguranță: build-urile de release sunt de obicei livrate cu ea dezactivată, iar chiar și când este activată ați transformat un bug de siguranță a memoriei într-un EIntOverflow care scapă din mijlocul unei rutine de validare. PDFium Component tratează aritmetica de lungime nesigură la fel cum tratează restul limitei, o disciplină acoperită mai larg în întărirea ABI-ului PDFium VCL și siguranța memoriei în Delphi

De ce trebuie ca o fereastră de lungime zero să transmită nil?

Pentru că @Data[Index] nu este o expresie legală pentru fiecare Index pe care validarea îl acceptă. Index = Length(Data) cu Count = 0 este o fereastră goală perfect bine formată la coada buffer-ului, iar un TBytes gol dă Index = 0 pe un array care nu are deloc niciun element zero. Luarea adresei în oricare caz indexează dincolo de sfârșit, sau dereferențiază un array dinamic nil. Așa că supraîncărcarea ramifică: Count = 0 produce un pointer nil, orice alt count produce @Data[Index]. Nil-ul curge apoi în supraîncărcarea cu pointer, a cărei propria gardă acceptă un pointer nil când dimensiunea este zero, iar încărcarea se termină în eroarea obișnuită "Cannot load PDF document" în loc de o încălcare de acces. Un apelant care a calculat o fereastră de zero octeți dintr-un container malformat primește o EPdfError curată, ce poate fi prinsă, ca orice altă intrare greșită

Împrumutat sau copiat: ce decide Buffered

Buffered selectează contractul de proprietate, și este singurul parametru de aici cu consecințe dincolo de apel. Cu Buffered = True, PDFium Component copiază fereastra selectată, și doar fereastra, în buffer-ul său intern înainte de încărcare. Containerul de 40 MB nu este copiat; PDF-ul de 312 KB este. Odată ce LoadDocument revine, puteți elibera, reutiliza sau suprascrie containerul imediat, deoarece componenta nu îl mai referențiază. Aceasta este implicit și alegerea corectă pentru aproape tot codul

Buffered = False predă @Data[Index] direct lui FPDF_LoadMemDocument64, iar PDFium păstrează acel pointer pe durata de viață a documentului, în loc să copieze octeții. Asta face încărcarea fără alocare, și face din întregul TBytes suport o resursă împrumutată. Trebuie să rămână viu și neschimbat până când UnloadDocument rulează sau Active devine False. Nu fereastra, întregul array: un array dinamic este numărat prin referință ca o unitate, iar lăsarea ultimei referințe să plece oriunde în codul dumneavoastră eliberează memoria pe care PDFium încă o citește. Setarea lui Length pe el este la fel de fatală, deoarece o realocare poate muta blocul. Enunțați asta în propria dumneavoastră documentație de API oriunde expuneți o astfel de încărcare, în același spirit ca orice altă graniță împrumut-versus-proprietate în cod Pascal; modul de eșec este identic cu pericolele de aliasing descrise în scurgerea de FillChar și string rezultat în Delphi, unde un buffer pare deținut și nu este

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;

Când fereastra de interval de octeți este instrumentul greșit

Fiți sinceri despre limită. Supraîncărcarea de interval de octeți presupune că containerul este deja complet în memorie, iar Count este un Integer, așa că o singură fereastră nu poate depăși doi gigabyte. Dacă containerul este o arhivă de 6 GB pe disc, sau sosește printr-un socket pe care nu îl puteți rebobina, această supraîncărcare nu vă poate ajuta, iar citirea întregului lucru în TBytes doar pentru a adresa o fereastră din interior anulează sensul. Exact acolo aparține calea FPDF_FILEACCESS, iar articolul despre streaming la cerere arată cum să expuneți o vedere decalată de offset a unui fișier ca o sursă de document personalizată. La fel, dacă octeții încorporați au nevoie de transformare înainte ca PDFium să îi vadă, decompresie, decriptare, un pas de dezambalare, atunci o copie reală este inevitabilă, iar Buffered = True pe array-ul transformat este răspunsul onest. Fereastra de interval de octeți dă roade în exact o formă: octeți PDF contigui, neschimbați, deja rezidenți, la un offset cunoscut

Dacă evaluați asta pentru un vizualizator, un panou de previzualizare sau un pipeline de recepție în lot, supraîncărcarea de interval de octeți și loader-ul de streaming sunt două din strategiile de încărcare pe care PDFium Component le livrează alături de încărcările din fișier, flux și pointer brut. Suprafața API completă, licențierea și suportul de versiune Delphi și C++Builder sunt documentate pe pagina de produs PDFium Component