Tekninen artikkeli

PDFium-tavualueen lataus upotetuille PDF-tiedostoille Delphissä

PDFium Component voi avata PDF:n, joka asuu suuremman puskurin sisällä, suoraan tavualueesta. Ylikuormitusversio LoadDocument(const Data: TBytes; Index, Count: Integer; Buffered: Boolean) osoittaa ikkunan paikallaan, joten alustavaa Copy-kutsua ei tarvita. Vastineeksi se pyytää sinua ymmärtämään yhden säännön: kun Buffered on False, taustalla oleva taulukko on lainattu, ei kopioitu

Tämä on eri mekanismi kuin takaisinkutsuvetoinen lähestymistapa, joka on kuvattu artikkelissa suurten PDF:ien suoratoisto tarpeen mukaan PDFium VCL:llä, joka antaa PDFiumille FPDF_FILEACCESS-lukijan ja antaa sen vetää lohkoja levyltä tarpeen mukaan. Se on tarkoitettu asiakirjoille, jotka ovat liian suuria mahtuakseen RAM-muistiin. Tämä on tarkoitettu asiakirjoille, jotka ovat jo RAM-muistissa, tunnetussa siirtymässä jonkin muun sisällä. Nämä kaksi täydentävät toisiaan, ja viimeinen osio selittää, kumpi tilanne kuuluu kummallekin

40 megatavun kopio, jota kukaan ei pyytänyt

Skenaario ilmenee kaikkialla, missä PDF:t matkustavat muiden muotojen sisällä. Postivarasto pitää viestirungot ja liitteet yhdessä tietueessa. Arkistosäilö ketjuttaa manifestin, muutaman kuvan ja PDF:n. Mukautettu siirtoprotokolla kehystää asiakirjan pituusetuliitteisen otsikon taakse. Jokaisessa tapauksessa päädyt pitämään yhtä suurta TBytes:iä ja tietämään, että PDF alkaa tavusta 1 182 336 ja kestää 312 kilotavua

Ennen kuin tavualue-ylikuormitusversio oli olemassa, idiomaattinen vastaus oli Copy(Data, Index, Count), joka varaa toisen taulukon ja memcpy:ää ikkunan siihen. Sitten annat tuon siivun LoadDocument:lle Buffered = True:lla, joka kopioi sen uudelleen komponentin yksityiseen puskuriin. Kaksi kopiota samoista tavuista, toinen niistä puhdasta seremoniaa, ja suuren postilaatikkoskannauksen kohdalla toistetaan jokaiselle viestille. Tavualue-ylikuormitusversio poistaa ensimmäisen kopion ehdoitta ja toisen valinnaisesti

Mitä tavualue-ylikuormitusversio todella tekee

Ylikuormitusversio on tarkoituksella ohut: se validoi, laskee yhden osoittimen ja delegoi LoadDocument:n osoitinmuodolle, jonka läpi koko perhe jo kulkee. Index on nollasta alkava, Count on tavupituus, ja Buffered on oletuksena True täsmälleen samoin kuin muissakin ylikuormitusversioissa. Yksiargumenttisesta LoadDocument(const Data: TBytes; Buffered: Boolean):sta on nyt itsestään tullut vain kutsu tähän arvoilla Index = 0 ja Count = Length(Data), joten on yksi validointipolku kahden sijaan

Sen kutsuminen näyttää koodilta, jota jo kirjoitit, miinus siivu

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;

Miksi Index plus Count ylivuotaa rajatarkistuksen?

Koska Index ja Count ovat molemmat Integer, eikä kahden suuren positiivisen Integer-arvon summa ole välttämättä suuri positiivinen Integer. Tämä on ylikuormitusversion tekninen ydin, ja se on ainoa paikka, jossa luonnolliselta näyttävä tarkistus on muistiturvallisuusaukko. Ilmeinen muotoilu on väärä

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

Käy epäonnistuva tapaus läpi. Ota Index = 2000000000 ja Count = 2000000000. Niiden todellinen summa on neljä miljardia, mutta 32-bittisessä etumerkillisessä aritmetiikassa tulos kiertyy täsmälleen arvoon miinus 294 967 296. Tuo arvo on mukavasti pienempi kuin Length(Data), joten väärä tarkistus läpäisee, @Data[Index] otetaan kaukana taulukon ulkopuolelta, ja PDFiumille annetaan villi osoitin plus kahden gigatavun pituus. Seuraa käyttöoikeusrikkomus hyvänä päivänä ja pahana päivänä liittymättömän prosessimuistin hiljainen jäsennys

Oikea järjestys korjaa tämän olemalla koskaan laskematta yhteen. Negatiiviset hylätään ennen kuin mitään indeksoidaan, joten @Data[Index] ei voi koskaan mennä taulukon alle. Sitten Index rajataan yksinään Length(Data):aa vasten, mikä takaa, että Length(Data) - Index on ei-negatiivinen Integer. Vasta sitten Count verrataan tuohon jäännökseen. Jokainen välituloksen arvo pysyy esitettävissä olevalla alueella, joten mikään käännösasetus ei voi muuttaa lopputulosta. Älä myöskään houkuttele itseäsi luottamaan {$Q+}-ylivuototarkistukseen turvaverkkona: julkaisukäännökset toimitetaan rutiininomaisesti sen ollessa pois päältä, ja vaikka se olisi päällä, olisit vain muuttanut muistiturvallisuusvirheen validointirutiinin keskeltä pakenevaksi EIntOverflow:ksi. PDFium Component kohtelee epäluotettua pituusaritmetiikkaa samalla tavalla kuin se kohtelee muuta rajaa, kurinalaisuus, joka käsitellään laajemmin artikkelissa PDFium VCL ABI:n ja muistiturvallisuuden vahvistaminen Delphissä

Miksi nollapituinen ikkuna täytyy antaa nil-arvona?

Koska @Data[Index] ei ole laillinen lauseke jokaiselle Index:lle, jonka validointi hyväksyy. Index = Length(Data), kun Count = 0, on täysin hyvin muodostettu tyhjä ikkuna puskurin hännässä, ja tyhjä TBytes antaa Index = 0:n taulukolle, jossa ei ole lainkaan elementtiä nolla. Osoitteen ottaminen kummassakin tapauksessa indeksoi lopun yli tai dereferoi nil-dynaamisen taulukon. Joten ylikuormitusversio haarautuu: Count = 0 tuottaa nil-osoittimen, mikä tahansa muu määrä tuottaa @Data[Index]:n. Nil virtaa sitten osoitinylikuormitusversioon, jonka oma vartija hyväksyy nil-osoittimen, kun koko on nolla, ja lataus päättyy tavalliseen "Cannot load PDF document" -virheeseen käyttöoikeusrikkomuksen sijaan. Kutsuja, joka laski nollatavuisen ikkunan väärin muodostetusta säilöstä, saa puhtaan, kiinniotettavan EPdfError:n kuten minkä tahansa muun huonon syötteen

Lainattu vai kopioitu: mitä Buffered päättää

Buffered valitsee omistussopimuksen, ja se on ainoa parametri tässä, jolla on seurauksia kutsun ulkopuolelle. Buffered = True:lla PDFium Component kopioi valitun ikkunan, ja vain ikkunan, sisäiseen puskuriinsa ennen lataamista. 40 megatavun säilöä ei kopioida; 312 kilotavun PDF kopioidaan. Heti kun LoadDocument palaa, voit vapauttaa, käyttää uudelleen tai ylikirjoittaa säilön välittömästi, koska komponentti ei enää viittaa siihen. Tämä on oletus ja oikea valinta lähes kaikelle koodille

Buffered = False antaa @Data[Index]:n suoraan FPDF_LoadMemDocument64:lle, ja PDFium pitää tuota osoitinta asiakirjan koko elinajan tavujen kopioimisen sijaan. Se tekee latauksesta varauksettoman, ja se tekee koko taustalla olevasta TBytes:istä lainatun resurssin. Sen täytyy pysyä elossa ja muokkaamattomana, kunnes UnloadDocument ajetaan tai Active menee False:ksi. Ei vain ikkunan, koko taulukon: dynaaminen taulukko on viittauslaskettu yksikkönä, ja viimeisen viittauksen päästäminen irti missä tahansa koodissasi vapauttaa muistin, jota PDFium yhä lukee. Sen Length:in asettaminen on yhtä kohtalokasta, koska uudelleenvaraus voi siirtää lohkoa. Kirjoita tämä omaan API-dokumentaatioosi aina kun paljastat tällaisen latauksen, samassa hengessä kuin mikä tahansa muu lainaa-vastaan-omista-raja Pascal-koodissa; vikamoodi on identtinen artikkelissa FillChar ja tulosmerkkijonon vuoto Delphissä kuvattujen aliasoinnin vaarojen kanssa, joissa puskuri näyttää omistetulta eikä ole

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;

Kun tavualueikkuna on väärä työkalu

Ole rehellinen rajasta. Tavualue-ylikuormitusversio olettaa säilön olevan jo kokonaan muistissa, ja Count on Integer, joten yksi ikkuna ei voi ylittää kahta gigatavua. Jos säilö on 6 gigatavun arkisto levyllä, tai saapuu pistokkeen kautta, jota et voi kelata takaisin, tämä ylikuormitusversio ei voi auttaa sinua, ja koko asian lukeminen TBytes:iin vain osoittaakseen ikkunan sen sisällä tekee tyhjäksi koko pointin. Juuri siihen FPDF_FILEACCESS-polku kuuluu, ja tarpeen-mukaan-suoratoisto-artikkeli näyttää, miten paljastetaan siirtymäsiirretty näkymä tiedostosta mukautettuna asiakirjalähteenä. Yhtä lailla, jos upotetut tavut tarvitsevat muunnoksen ennen kuin PDFium näkee ne, purku, salauksen purku, uudelleenkääntövaihe, silloin todellinen kopio on väistämätön, ja Buffered = True muunnetulle taulukolle on rehellinen vastaus. Tavualueikkuna maksaa itsensä takaisin täsmälleen yhdessä muodossa: yhtenäinen, muokkaamaton PDF-tavut, jo residentti, tunnetussa siirtymässä

Jos arvioit tätä katseluohjelmalle, esikatselupaneelille tai eräkäsittelyn saantiputkelle, tavualue-ylikuormitusversio ja suoratoistava lataaja ovat kaksi latausstrategiaa, jotka PDFium Component toimittaa tiedosto-, virta- ja raakaosoitin-latausten rinnalla. Täysi API-pinta, lisensointi sekä Delphi- ja C++Builder-versiotuki on dokumentoitu PDFium Component -tuotesivulla