Tehnični članak

Nalaganje PDFium bajtnih obsegov za vgrajene PDF v Delphiju

PDFium Component lahko odpre PDF, ki živi znotraj večjega medpomnilnika, neposredno iz bajtnega obsega. Preobtežitev LoadDocument(const Data: TBytes; Index, Count: Integer; Buffered: Boolean) naslovi okno na mestu, tako da predhoden Copy ni potreben. V zameno vas prosi, da razumete eno pravilo: kadar je Buffered False, je podporno polje izposojeno, ne kopirano

To je drugačen mehanizem od pristopa, poganjanega s povratnimi klici, opisanega v pretakanju velikih PDF-jev na zahtevo s PDFium VCL, ki PDFium izroči bralnik FPDF_FILEACCESS in mu pusti, da bloke vleče z diska, ko jih potrebuje. Tisto je za dokumente, prevelike, da bi jih hranili v RAM. Ta je za dokumente, ki so že v RAM, na znanem odmiku znotraj nečesa drugega. Oba sta dopolnilna, zadnji razdelek pa razloži, kateri situaciji pripada kateri

40 MB kopija, ki je nihče ni zahteval

Scenarij se pojavi povsod, kjer PDF-ji potujejo znotraj drugih formatov. Poštna shramba hrani telesa sporočil in priloge v enem zapisu. Arhivski vsebnik združi manifest, nekaj slik in PDF. Protokol po meri okvirja dokument za glavo s predpono dolžine. V vsakem primeru pristanete z enim velikim TBytes in vedenjem, da se PDF začne na bajtu 1 182 336 in teče 312 kilobajtov

Preden je preobtežitev bajtnega obsega obstajala, je bil idiomatičen odgovor Copy(Data, Index, Count), ki dodeli drugo polje in memcpy-ja okno vanj. Nato ta izsek izročite LoadDocument z Buffered = True, ki ga ponovno kopira v zasebni medpomnilnik komponente. Dve kopiji istih bajtov, ena od njih čista ceremonija, na velikem pregledu poštnega predala pa se ponovi za vsako sporočilo. Preobtežitev bajtnega obsega odstrani prvo kopijo brezpogojno in drugo neobvezno

Kaj preobtežitev bajtnega obsega dejansko počne

Preobtežitev je namerno tenka: preveri, izračuna en kazalec in prepusti kazalčni obliki LoadDocument, skozi katero se že izliva cela družina. Index je z ničlo osnovan, Count je dolžina v bajtih, Buffered pa je privzeto True, natanko tako kot pri drugih preobtežitvah. Enoargumentna LoadDocument(const Data: TBytes; Buffered: Boolean) je zdaj sama le klic te z Index = 0 in Count = Length(Data), tako da obstaja ena pot preverjanja namesto dveh

Klicanje izgleda kot koda, ki ste jo že pisali, minus izsek

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;

Zakaj Index plus Count preplavi preverjanje meja?

Ker sta Index in Count oba Integer, vsota dveh velikih pozitivnih vrednosti Integer pa ni nujno velik pozitiven Integer. To je tehnično jedro preobtežitve, in edino mesto, kjer je naravno izgledajoče preverjanje luknja v varnosti pomnilnika. Očitna formulacija je napačna

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

Preglejmo odpovedujoč primer. Vzemite Index = 2000000000 in Count = 2000000000. Njuna prava vsota je štiri milijarde, v 32-bitni predznačeni aritmetiki pa se rezultat preplavi natanko na minus 294 967 296. Ta vrednost je udobno manjša od Length(Data), zato napačno preverjanje mine, @Data[Index] se vzame daleč zunaj polja, PDFium pa se izroči divji kazalec plus dolžina dveh gigabajtov. Kar sledi, je dostopna kršitev ob dobrem dnevu in tiho razčlenjevanje nepovezanega pomnilnika procesa ob slabem

Pravilni vrstni red to popravi tako, da nikoli ne sešteva. Negativne vrednosti so zavrnjene, preden se karkoli indeksira, tako da se @Data[Index] nikoli ne more vzeti pod polje. Nato je Index omejen sam po sebi proti Length(Data), kar zagotovi, da je Length(Data) - Index nenegativen Integer. Šele nato se Count primerja proti temu ostanku. Vsaka vmesna vrednost ostane znotraj predstavljivega obsega, tako da nobena konfiguracija gradnje ne more spremeniti izida. Ne bodite v skušnjavi, da bi se zanašali na preverjanje prekoračitve {$Q+} kot varnostno mrežo: izdajne gradnje jo rutinsko dostavljajo izklopljeno, tudi ko je vklopljena, pa ste napako varnosti pomnilnika pretvorili v EIntOverflow, ki uhaja iz sredine rutine preverjanja. PDFium Component obravnava nezaupanja vredno aritmetiko dolžine na enak način kot preostanek meje, disciplino, širše pokrito v utrjevanju PDFium VCL ABI in varnosti pomnilnika v Delphiju

Zakaj mora okno z ničelno dolžino podati nil?

Ker @Data[Index] ni zakonit izraz za vsak Index, ki ga preverjanje sprejme. Index = Length(Data) s Count = 0 je povsem pravilno oblikovano prazno okno na repu medpomnilnika, prazen TBytes pa da Index = 0 na polju, ki sploh nima elementa nič. Vzemanje naslova v obeh primerih indeksira mimo konca ali dereferencira nil dinamično polje. Zato se preobtežitev razveji: Count = 0 da nil kazalec, vsako drugo število da @Data[Index]. Nil nato steče v kazalčno preobtežitev, katere lastna varovalka sprejme nil kazalec, kadar je velikost nič, nalaganje pa se konča z navadno napako "Cannot load PDF document" namesto dostopne kršitve. Klicatelj, ki je izračunal okno z nič bajti iz nepravilno oblikovanega vsebnika, dobi čist, ulovljiv EPdfError kot kateri koli drug slab vhod

Izposojeno ali kopirano: kaj odloči Buffered

Buffered izbere pogodbo lastništva, in je edini parameter tukaj s posledicami onkraj klica. Z Buffered = True PDFium Component kopira izbrano okno, in le okno, v svoj notranji medpomnilnik pred nalaganjem. 40 MB vsebnik se ne kopira; 312 KB PDF se. Ko se LoadDocument vrne, lahko vsebnik takoj sprostite, ponovno uporabite ali prepišete, ker komponenta nanj ne referencira več. To je privzeto in prava izbira za skoraj vso kodo

Buffered = False poda @Data[Index] naravnost FPDF_LoadMemDocument64, PDFium pa ta kazalec obdrži za življenjsko dobo dokumenta namesto da bi kopiral bajte. To naredi nalaganje brez alokacij, celotno podporno TBytes pa spremeni v izposojen vir. Mora ostati živo in nespremenjeno, dokler UnloadDocument ne teče ali Active ne postane False. Ne okno, celotno polje: dinamično polje je enota s štetjem referenc, izpust zadnje reference kjer koli v vaši kodi pa sprosti pomnilnik, ki ga PDFium še vedno bere. Nastavitev Length nanj je enako usodna, ker lahko realokacija premakne blok. Navedite to v svoji lastni API dokumentaciji, kjer koli izpostavite tako nalaganje, v istem duhu kot katera koli druga meja izposoje proti lastništvu v kodi Pascal; način odpovedi je enak nevarnostim aliasinga, opisanim v uhajanju FillChar in nizu rezultata v Delphiju, kjer je medpomnilnik videti last, pa ni

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;

Kdaj je okno bajtnega obsega napačno orodje

Bodite iskreni glede meje. Preobtežitev bajtnega obsega predpostavlja, da je vsebnik že v celoti v pomnilniku, Count pa je Integer, tako da eno okno ne more preseči dveh gigabajtov. Če je vsebnik 6 GB arhiv na disku ali prihaja preko vtičnice, ki je ne morete previti nazaj, vam ta preobtežitev ne more pomagati, branje celote v TBytes zgolj zato, da bi naslovili okno znotraj, pa porazi bistvo. Prav tam spada pot FPDF_FILEACCESS, članek o pretakanju na zahtevo pa pokaže, kako izpostaviti pogled datoteke z zamaknjenim odmikom kot vir dokumenta po meri. Enako, če vgrajeni bajti potrebujejo preoblikovanje, preden jih PDFium vidi, dekompresijo, dešifriranje, korak razpakiranja, je prava kopija neizogibna, Buffered = True na preoblikovanem polju pa je iskren odgovor. Okno bajtnega obsega se izplača natanko v eni obliki: sosednji, nespremenjeni bajti PDF, že rezidenčni, na znanem odmiku

Če to ocenjujete za pregledovalnik, predogledno ploščo ali paketni cevovod za sprejem, sta preobtežitev bajtnega obsega in pretakajoč nalagalnik dve od strategij nalaganja, ki jih PDFium Component dostavlja poleg nalaganj iz datoteke, toka in surovega kazalca. Celotna API površina, licenciranje in podpora različicam Delphi in C++Builder so dokumentirani na strani izdelka PDFium Component