PDFium Component kan een PDF openen die zich binnen een grotere buffer bevindt, rechtstreeks vanuit een bytebereik. De overload LoadDocument(const Data: TBytes; Index, Count: Integer; Buffered: Boolean) adresseert een venster ter plekke, dus is er geen voorafgaande Copy nodig. In ruil daarvoor vraagt hij je één regel te begrijpen: wanneer Buffered False is, is de onderliggende array geleend, niet gekopieerd
Dit is een ander mechanisme dan de callback-gedreven aanpak beschreven in het streamen van grote PDF's on-demand met PDFium VCL, die PDFium een FPDF_FILEACCESS-reader geeft en het blokken van schijf laat ophalen zoals nodig. Dat is voor documenten te groot om in RAM te houden. Dit is voor documenten die al in RAM staan, op een bekende offset binnen iets anders. De twee vullen elkaar aan, en de laatste sectie legt uit welke situatie bij welke hoort
De kopie van 40 MB die niemand vroeg
Het scenario duikt overal op waar PDF's binnen andere formaten reizen. Een mailopslag bewaart berichttekst en bijlagen in één record. Een archiefcontainer voegt een manifest, een paar afbeeldingen en een PDF aaneen. Een aangepast draadprotocol omkadert een document achter een lengte-voorafgegane header. In elk geval houd je één grote TBytes vast en weet je dat de PDF begint bij byte 1.182.336 en 312 kilobytes doorloopt
Voordat de byte-range-overload bestond, was het idiomatische antwoord Copy(Data, Index, Count), dat een tweede array alloceert en het venster erin memcpy't. Je geeft die slice dan aan LoadDocument met Buffered = True, wat het nogmaals kopieert naar de private buffer van het component. Twee kopieën van dezelfde bytes, waarvan één pure ceremonie, en bij een grote mailboxscan herhaald voor elk bericht. De byte-range-overload verwijdert de eerste kopie onvoorwaardelijk en de tweede optioneel
Wat de byte-range-overload daadwerkelijk doet
De overload is bewust dun: hij valideert, berekent één pointer, en delegeert naar de pointervorm van LoadDocument waar de hele familie al doorheen loopt. Index is nulgebaseerd, Count is een bytelengte, en Buffered staat standaard op True precies zoals op de andere overloads. De overload met één argument LoadDocument(const Data: TBytes; Buffered: Boolean) is zelf nu gewoon een aanroep naar deze met Index = 0 en Count = Length(Data), dus er is één validatiepad in plaats van twee
Hem aanroepen lijkt op de code die je al schreef, min de slice
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;
Waarom laat Index plus Count de grenscontrole overlopen?
Omdat Index en Count beide Integer zijn, en de som van twee grote positieve Integer-waarden niet noodzakelijk een grote positieve Integer is. Dit is de technische kern van de overload, en het is de ene plek waar een natuurlijk ogende controle een geheugenveiligheidsgat is. De voor de hand liggende formulering is fout
// 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');
Werk het falende geval uit. Neem Index = 2000000000 en Count = 2000000000. Hun ware som is vier miljard, maar in 32-bit signed rekenkunde slaat het resultaat om naar precies minus 294.967.296. Die waarde is ruim kleiner dan Length(Data), dus de foute controle slaagt, @Data[Index] wordt genomen ver buiten de array, en PDFium krijgt een wilde pointer plus een lengte van twee gigabyte. Wat volgt is op een goede dag een access violation en op een slechte dag het stilzwijgend parsen van niet-gerelateerd procesgeheugen
De correcte volgorde repareert dit door nooit op te tellen. Negatieve waarden worden verworpen voordat er ook maar geïndexeerd wordt, dus @Data[Index] kan nooit onder de array genomen worden. Dan wordt Index op zichzelf begrensd tegen Length(Data), wat garandeert dat Length(Data) - Index een niet-negatieve Integer is. Pas dan wordt Count vergeleken met dat restant. Elke tussenwaarde blijft binnen het representeerbare bereik, dus geen enkele buildconfiguratie kan het resultaat veranderen. Laat je ook niet verleiden om te vertrouwen op {$Q+}-overflowcontrole als vangnet: releasebuilds worden routinematig verzonden met die uitgeschakeld, en zelfs wanneer die aan staat, heb je een geheugenveiligheidsbug omgezet in een EIntOverflow die uit het midden van een validatieroutine ontsnapt. PDFium Component behandelt onvertrouwde lengterekenkunde op dezelfde manier als de rest van de grens, een discipline breder behandeld in het hardenen van de PDFium VCL ABI en geheugenveiligheid in Delphi
Waarom moet een venster met lengte nul nil doorgeven?
Omdat @Data[Index] geen legale expressie is voor elke Index die de validatie accepteert. Index = Length(Data) met Count = 0 is een volkomen welgevormd leeg venster aan de staart van de buffer, en een lege TBytes geeft Index = 0 op een array die helemaal geen element nul heeft. Het adres nemen in beide gevallen indexeert voorbij het einde, of dereferentieert een nil dynamische array. Dus vertakt de overload: Count = 0 levert een nil-pointer op, elke andere count levert @Data[Index] op. De nil stroomt dan de pointeroverload in, waarvan de eigen bewaker een nil-pointer accepteert wanneer de grootte nul is, en de load eindigt in de gewone fout "Cannot load PDF document" in plaats van een access violation. Een aanroeper die een venster van nul bytes berekende uit een misvormde container krijgt een schone, opvangbare EPdfError zoals elke andere slechte invoer
Geleend of gekopieerd: wat Buffered bepaalt
Buffered selecteert het eigendomscontract, en het is hier de enige parameter met gevolgen voorbij de aanroep. Met Buffered = True kopieert PDFium Component het geselecteerde venster, en alleen het venster, naar zijn interne buffer voordat het laadt. De container van 40 MB wordt niet gekopieerd; de PDF van 312 KB wel. Zodra LoadDocument terugkeert, mag je de container onmiddellijk vrijgeven, hergebruiken of overschrijven, omdat het component er niet meer naar verwijst. Dit is de standaard en de juiste keuze voor bijna alle code
Buffered = False geeft @Data[Index] rechtstreeks aan FPDF_LoadMemDocument64, en PDFium houdt die pointer vast voor de levensduur van het document in plaats van de bytes te kopiëren. Dat maakt de load allocatievrij, en het maakt de hele onderliggende TBytes een geleende resource. Ze moet levend en ongewijzigd blijven totdat UnloadDocument draait of Active False wordt. Niet het venster, de hele array: een dynamische array is reference-counted als een eenheid, en de laatste referentie ergens in je code laten vervallen geeft het geheugen vrij dat PDFium nog aan het lezen is. Length erop instellen is net zo fataal, omdat een reallocatie het blok kan verplaatsen. Vermeld dit in je eigen API-documentatie overal waar je zo'n load blootstelt, in dezelfde geest als elke andere lenen-versus-bezitten-grens in Pascal-code; het faalpatroon is identiek aan de aliasing-gevaren beschreven in de FillChar- en resultaatstring-lek in Delphi, waar een buffer eigendom lijkt en dat niet is
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;
Wanneer het byte-range-venster het verkeerde gereedschap is
Wees eerlijk over de grens. De byte-range-overload gaat ervan uit dat de container al volledig in het geheugen staat, en Count is een Integer, dus één venster kan niet groter zijn dan twee gigabyte. Als de container een archief van 6 GB op schijf is, of binnenkomt over een socket die je niet kunt terugspoelen, kan deze overload je niet helpen en verslaat het geheel inlezen naar TBytes alleen om een venster erbinnen te adresseren het hele doel. Dat is precies waar het FPDF_FILEACCESS-pad hoort, en het artikel over on-demand streaming laat zien hoe je een offset-verschoven weergave van een bestand blootstelt als een aangepaste documentbron. Evenzo, als de ingebedde bytes transformatie nodig hebben voordat PDFium ze ziet, decompressie, decryptie, een unwrap-stap, dan is een echte kopie onvermijdelijk en is Buffered = True op de getransformeerde array het eerlijke antwoord. Het byte-range-venster betaalt zich uit in precies één vorm: aaneengesloten, ongewijzigde PDF-bytes, al resident, op een bekende offset
Evalueer je dit voor een viewer, een voorbeeldvenster of een batchintakepipeline, dan zijn de byte-range-overload en de streaminglader twee van de laadstrategieën die PDFium Component levert naast bestand-, stream- en ruwe-pointer-loads. Het volledige API-oppervlak, licensering en ondersteuning voor Delphi- en C++Builder-versies staan gedocumenteerd op de productpagina van PDFium Component