A PDFium Component képes megnyitni egy nagyobb pufferen belül élő PDF-et közvetlenül egy bájttartományból. A LoadDocument(const Data: TBytes; Index, Count: Integer; Buffered: Boolean) túlterhelés a helyén címez meg egy ablakot, így nincs szükség előzetes Copy-ra. Cserébe azt kéri, hogy megértsünk egy szabályt: amikor a Buffered False, a mögöttes tömb kölcsönzött, nem másolt
Ez más mechanizmus, mint a nagy PDF-ek igény szerinti streamelése PDFium VCL-lel cikkben leírt visszahívás-vezérelt megközelítés, amely egy FPDF_FILEACCESS olvasót ad át a PDFiumnak, és hagyja, hogy blokkokat húzzon a lemezről igény szerint. Az a dokumentumokhoz való, amelyek túl nagyok ahhoz, hogy RAM-ban elférjenek. Ez a dokumentumokhoz való, amelyek már RAM-ban vannak, egy ismert eltolásnál ülve valami más belsejében. A kettő kiegészíti egymást, az utolsó szakasz pedig elmagyarázza, melyik helyzet melyikhez tartozik
A 40 MB-os másolat, amelyet senki nem kért
A forgatókönyv mindenhol felbukkan, ahol PDF-ek más formátumokon belül utaznak. Egy levéltár egy rekordban tartja az üzenettörzseket és a mellékleteket. Egy archívumkonténer összefűz egy manifestet, néhány képet és egy PDF-et. Egy egyedi vezetékprotokoll egy dokumentumot hosszal ellátott fejléc mögé keretez. Minden esetben egy nagy TBytes-t tartunk a kezünkben, és tudjuk, hogy a PDF az 1 182 336. bájtnál kezdődik, és 312 kilobájtig fut
Mielőtt a bájttartomány-túlterhelés létezett volna, az idiomatikus válasz a Copy(Data, Index, Count) volt, amely egy második tömböt allokál, és memcpy-vel másolja bele az ablakot. Ezt a szeletet ezután átadjuk a LoadDocument-nek Buffered = True-val, ami újra bemásolja a komponens privát pufferébe. Ugyanazon bájtok két másolata, az egyik tiszta szertartás, egy nagy postafiók-vizsgálatnál minden üzenetre megismételve. A bájttartomány-túlterhelés feltétel nélkül eltávolítja az elsőt, és opcionálisan a másodikat
Mit tesz valójában a bájttartomány-túlterhelés
A túlterhelés szándékosan vékony: ellenőriz, kiszámít egy mutatót, és a LoadDocument mutató formájának delegálja, amelyen keresztül az egész család már úgyis áttorkollik. Az Index nulla-alapú, a Count egy bájthossz, a Buffered pedig alapértelmezetten True, pontosan úgy, ahogy a többi túlterhelésnél. Az egyargumentumú LoadDocument(const Data: TBytes; Buffered: Boolean) maga most csupán egy hívás ehhez, Index = 0-val és Count = Length(Data)-val, így egyetlen ellenőrzési útvonal van kettő helyett
A hívása úgy néz ki, mint a kód, amelyet már úgyis írtunk, mínusz a szelet
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;
Miért csordul túl az Index plusz Count a határellenőrzésnél?
Mert az Index és a Count is Integer, és két nagy pozitív Integer érték összege nem feltétlenül egy nagy pozitív Integer. Ez a túlterhelés technikai magja, és ez az egyetlen hely, ahol egy természetesnek tűnő ellenőrzés memóriabiztonsági lyuk. A nyilvánvaló megfogalmazás hibás
// 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');
Vezessük végig a bukó esetet. Vegyük az Index = 2000000000-t és a Count = 2000000000-t. Valódi összegük négymilliárd, de 32 bites előjeles aritmetikában az eredmény pontosan mínusz 294 967 296-ra fordul körbe. Ez az érték kényelmesen kisebb, mint a Length(Data), így a hibás ellenőrzés átmegy, a @Data[Index] messze a tömbön kívülre kerül, a PDFium pedig egy vad mutatót kap plusz egy kétgigabájtos hosszt. Ami ezután következik, az egy jó napon egy hozzáférési kivétel, egy rossz napon pedig nem kapcsolódó folyamatmemória csendes elemzése
A helyes sorrend úgy javítja ezt, hogy soha nem ad össze. A negatívak elutasításra kerülnek, mielőtt bármit is indexelnénk, így a @Data[Index] soha nem vehető a tömb alatt. Ezután az Index önmagában van korlátozva a Length(Data)-hoz képest, ami garantálja, hogy a Length(Data) - Index egy nem negatív Integer. Csak ezután kerül a Count összehasonlításra ezzel a maradékkal. Minden köztes érték a reprezentálható tartományon belül marad, így semmilyen build-konfiguráció nem tudja megváltoztatni az eredményt. Ne legyünk kísértve arra, hogy a {$Q+} túlcsordulás-ellenőrzésre támaszkodjunk biztonsági hálóként sem: a release buildek rendszeresen kikapcsolva szállítanak vele, és még ha be is van kapcsolva, akkor is csak egy memóriabiztonsági hibát alakítottunk egy EIntOverflow-vá, amely egy ellenőrzőrutin közepéről szökik ki. A PDFium Component ugyanúgy kezeli a nem megbízható hosszaritmetikát, mint a határ többi részét, ezt a fegyelmet szélesebben tárgyalja a a PDFium VCL ABI megerősítése és memóriabiztonság Delphiben
Miért kell egy nulla hosszúságú ablaknak nil-t átadnia?
Mert a @Data[Index] nem legális kifejezés minden Index-re, amelyet az ellenőrzés elfogad. Az Index = Length(Data), Count = 0 mellett egy tökéletesen jólformázott üres ablak a puffer végén, egy üres TBytes pedig Index = 0-t ad egy olyan tömbön, amelynek egyáltalán nincs nulladik eleme. A cím vétele bármelyik esetben túlindexel a végén, vagy egy nil dinamikus tömböt dereferál. Így a túlterhelés elágazik: a Count = 0 egy nil mutatót ad, bármely más darabszám a @Data[Index]-et. A nil ezután a mutató-túlterhelésbe folyik, amelynek saját őre elfogad egy nil mutatót, amikor a méret nulla, és a betöltés a szokásos "Cannot load PDF document" hibával ér véget hozzáférési kivétel helyett. Egy hívó, amely egy hibás konténerből számított ki egy nullabájtos ablakot, egy tiszta, elkapható EPdfError-t kap, mint bármely más hibás bemenetnél
Kölcsönzött vagy másolt: mit dönt el a Buffered
A Buffered választja ki a tulajdonjogi szerződést, és ez az egyetlen paraméter itt, amelynek a hívásán túlmutató következményei vannak. Buffered = True mellett a PDFium Component a kiválasztott ablakot, és csak az ablakot másolja a belső pufferébe, mielőtt betöltene. A 40 MB-os konténer nem kerül másolásra; a 312 KB-os PDF igen. Amint a LoadDocument visszatér, azonnal felszabadíthatjuk, újrahasznosíthatjuk vagy felülírhatjuk a konténert, mert a komponens már nem hivatkozik rá. Ez az alapértelmezett, és szinte minden kódnál a helyes választás
A Buffered = False közvetlenül a @Data[Index]-et adja át a FPDF_LoadMemDocument64-nek, és a PDFium megtartja ezt a mutatót a dokumentum élettartamára ahelyett, hogy másolná a bájtokat. Ez allokációmentessé teszi a betöltést, és a teljes mögöttes TBytes-t kölcsönzött erőforrássá teszi. Élve és módosítatlanul kell maradnia, amíg az UnloadDocument le nem fut, vagy az Active False-ra nem vált. Nem az ablaknak, a teljes tömbnek: egy dinamikus tömb egységként referenciaszámlált, és ha az utolsó hivatkozását bárhol elengedjük a kódunkban, felszabadítja azt a memóriát, amelyet a PDFium még mindig olvas. A Length beállítása rajta ugyanígy végzetes, mert egy újraallokáció elmozdíthatja a blokkot. Rögzítsük ezt a saját API-dokumentációnkban mindenhol, ahol ilyen betöltést teszünk elérhetővé, ugyanabban a szellemben, mint bármely más kölcsönzés-versus-tulajdonlás határt Pascal kódban; a hibamód azonos a a FillChar és az eredmény-sztring szivárgás Delphiben cikkben leírt aliasing-veszélyekkel, ahol egy puffer tulajdonoltnak látszik, de nem az
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;
Amikor a bájttartomány-ablak a rossz eszköz
Legyünk őszinték a határt illetően. A bájttartomány-túlterhelés feltételezi, hogy a konténer már teljesen a memóriában van, a Count pedig egy Integer, így egyetlen ablak sem haladhatja meg a kétgigabájtot. Ha a konténer egy 6 GB-os archívum a lemezen, vagy egy socketen érkezik, amelyet nem tudunk visszatekerni, ez a túlterhelés nem tud segíteni, és az egész beolvasása egy TBytes-be csak azért, hogy megcímezzünk benne egy ablakot, legyőzi a lényeget. Pontosan itt van a helye az FPDF_FILEACCESS útvonalnak, és az igény szerinti streamelésről szóló cikk megmutatja, hogyan tegyünk közzé egy eltolt nézetet egy fájlról egyedi dokumentumforrásként. Ugyanígy, ha a beágyazott bájtoknak átalakításra van szükségük, mielőtt a PDFium látná őket, kitömörítés, visszafejtés, egy kicsomagolási lépés, akkor egy valódi másolat elkerülhetetlen, és a Buffered = True az átalakított tömbön a becsületes válasz. A bájttartomány-ablak pontosan egy alakban térül meg: összefüggő, módosítatlan PDF-bájtok, már rezidensek, egy ismert eltolásnál
Ha ezt egy megjelenítőhöz, egy előnézeti panelhez vagy egy kötegelt befogadási pipeline-hoz mérlegeljük, a bájttartomány-túlterhelés és a streamelő betöltő a PDFium Component által kínált betöltési stratégiák két fajtája, a fájl-, folyam- és nyers mutató betöltések mellett. A teljes API-felület, a licencelés és a Delphi és C++Builder verziótámogatás a PDFium Component termékoldalon van dokumentálva