Techninis straipsnis

PDFium baitų intervalo įkėlimas įterptiems PDF Delphi

PDFium Component gali atverti PDF, gyvenantį didesniame buferyje, tiesiai iš baitų intervalo. Perkrova LoadDocument(const Data: TBytes; Index, Count: Integer; Buffered: Boolean) adresuoja langą vietoje, todėl jokio išankstinio Copy nereikia. Mainais ji prašo suprasti vieną taisyklę: kai Buffered yra False, pagrindinis masyvas — pasiskolintas, ne kopijuotas

Tai — kitoks mechanizmas nei atgalinio iškvietimo grindžiamas metodas, aprašytas straipsnyje apie didelių PDF srautinį įkėlimą pagal poreikį su PDFium VCL, kuris PDFium paduoda FPDF_FILEACCESS skaitytuvą ir leidžia jam traukti blokus iš disko pagal poreikį. Tas skirtas dokumentams, per dideliems, kad tilptų RAM atmintyje visai. Šis skirtas dokumentams, jau esantiems RAM atmintyje, žinomame poslinkyje kažko kito viduje. Šie du papildo vienas kitą, o paskutinis skyrius paaiškina, kuri situacija priklauso kuriai

40 MB kopija, kurios niekas neprašė

Scenarijus pasirodo visur, kur PDF failai keliauja kitų formatų viduje. Pašto saugykla laiko pranešimų kūnus ir priedus viename įraše. Archyvo konteineris sujungia manifestą, kelis vaizdus ir PDF. Pasirinktinis laidinis protokolas apgaubia dokumentą už ilgio prefikso antraštės. Kiekvienu atveju jūs turite vieną didelį TBytes ir žinote, kad PDF prasideda baite 1 182 336 ir trunka 312 kilobaitų

Prieš baitų intervalo perkrovai egzistuojant, idiomatinis atsakymas buvo Copy(Data, Index, Count), kuris paskirstyto antrą masyvą ir memcpy'ina langą į jį. Tada perduodate tą griežinį LoadDocument su Buffered = True, kas jį kopijuoja dar kartą į komponento privatų buferį. Dvi tų pačių baitų kopijos, viena iš jų — grynas ceremonijas, ir dideliame pašto dėžutės skenavime pakartojama kiekvienam pranešimui. Baitų intervalo perkrova pašalina pirmą kopiją besąlygiškai, o antrą — pasirinktinai

Ką iš tikrųjų daro baitų intervalo perkrova

Perkrova sąmoningai plona: ji patvirtina, apskaičiuoja vieną rodyklę ir deleguoja rodyklinei LoadDocument formai, per kurią jau eina visa šeima. Index — nuo nulio skaičiuojamas, Count — baitų ilgis, o Buffered pagal nutylėjimą — True, lygiai taip pat, kaip kitose perkrovose. Vieno argumento LoadDocument(const Data: TBytes; Buffered: Boolean) pati dabar tėra šio iškvietimas su Index = 0 ir Count = Length(Data), todėl yra vienas patvirtinimo kelias, ne du

Jos iškvietimas atrodo kaip kodas, kurį jau rašėte, minus griežinys

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;

Kodėl Index plius Count perpildo ribų patikrą?

Todėl, kad Index ir Count abu — Integer, o dviejų didelių teigiamų Integer reikšmių suma nebūtinai yra didelė teigiama Integer reikšmė. Tai — techninis šios perkrovos branduolys, ir tai — vienintelė vieta, kur natūraliai atrodanti patikra yra atminties saugumo skylė. Akivaizdi formuluotė — neteisinga

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

Peržiūrėkite nepavykstantį atvejį. Paimkite Index = 2000000000 ir Count = 2000000000. Jų tikra suma — keturi milijardai, bet 32 bitų su ženklu aritmetikoje rezultatas apsisuka lygiai iki minus 294 967 296. Ta reikšmė patogiai mažesnė nei Length(Data), todėl neteisinga patikra praeina, @Data[Index] paimamas toli už masyvo ribų, o PDFium gauna laukinę rodyklę plius dviejų gigabaitų ilgį. Kas seka — prieigos pažeidimas geros dienos atveju ir tylus nesusijusios proceso atminties analizavimas blogos dienos atveju

Teisinga tvarka tai pataiso, niekada nesudėdama. Neigiamos reikšmės atmetamos prieš bet ką indeksuojant, todėl @Data[Index] niekada negalima paimti žemiau masyvo. Tada Index apribojamas atskirai pagal Length(Data), kas garantuoja, kad Length(Data) - Index yra ne-neigiamas Integer. Tik tada Count lyginamas su tuo likučiu. Kiekviena tarpinė reikšmė lieka atvaizduojamame diapazone, todėl jokia versijos konfigūracija negali pakeisti rezultato. Taip pat negalima gundytis pasikliauti {$Q+} perpildymo patikra kaip apsauga: išleidimo versijos rutiniškai siunčiamos ją išjungus, ir net kai ji įjungta, jūs paverčiate atminties saugumo klaidą EIntOverflow, ištrūkstančia iš patvirtinimo procedūros vidurio. PDFium Component elgiasi su nepatikima ilgio aritmetika taip pat, kaip ir su likusia riba, ir šią discipliną plačiau aprašo straipsnis apie PDFium VCL ABI ir atminties saugumo sutvirtinimą Delphi

Kodėl nulinio ilgio langas turi perduoti nil?

Todėl, kad @Data[Index] nėra teisėta išraiška kiekvienam Index, kurį patvirtinimas priima. Index = Length(Data) su Count = 0 yra visiškai teisingai suformuotas tuščias langas buferio gale, o tuščias TBytes duoda Index = 0 masyve, kuris apskritai neturi nulinio elemento. Adreso paėmimas abiem atvejais indeksuoja už galo arba dereferencija nil dinaminį masyvą. Todėl perkrova šakojasi: Count = 0 duoda nil rodyklę, bet koks kitas skaičius duoda @Data[Index]. Tada nil patenka į rodyklinę perkrovą, kurios pačios apsauga priima nil rodyklę, kai dydis — nulis, ir įkėlimas baigiasi įprasta „Cannot load PDF document“ klaida, o ne prieigos pažeidimu. Iškviečiantysis, apskaičiavęs nulinio baito langą iš netaisyklingo konteinerio, gauna švarią, sugaunamą EPdfError kaip ir bet kokia kita neteisinga įvestis

Pasiskolinta ar kopijuota: ką nusprendžia Buffered

Buffered parenka nuosavybės kontraktą, ir tai — vienintelis parametras čia, turintis pasekmes už paties kvietimo ribų. Su Buffered = True, PDFium Component nukopijuoja pasirinktą langą, ir tik langą, į savo vidinį buferį prieš įkeliant. 40 MB konteineris nekopijuojamas; 312 KB PDF — kopijuojamas. Kai LoadDocument grįžta, galite iš karto paleisti, panaudoti pakartotinai ar perrašyti konteinerį, nes komponentas jo daugiau nebenaudoja. Tai — numatytoji ir teisinga beveik viso kodo pasirinktis

Buffered = False perduoda @Data[Index] tiesiai FPDF_LoadMemDocument64, ir PDFium išlaiko tą rodyklę visą dokumento gyvavimo laiką, vietoj baitų kopijavimo. Tai padaro įkėlimą be paskirstymo, ir tai paverčia visą pagrindinį TBytes pasiskolintu resursu. Jis turi likti gyvas ir nepakeistas, kol UnloadDocument pasileidžia arba Active tampa False. Ne langas — visas masyvas: dinaminis masyvas skaičiuojamas su nuoroda kaip vienetas, o paleidus paskutinę nuorodą bet kur jūsų kode, paleidžiama atmintis, kurią PDFium vis dar skaito. Length nustatymas jam — lygiai taip pat mirtinas, nes perpaskirstymas gali perkelti bloką. Tai nurodykite savo pačių API dokumentacijoje visur, kur atveriate tokį įkėlimą, ta pačia dvasia kaip ir bet kuri kita pasiskolinimo-prieš-nuosavybę riba Pascal kode; gedimo tipas identiškas aliasavimo pavojams, aprašytiems straipsnyje apie FillChar ir rezultato eilutės nutekėjimą Delphi, kur buferis atrodo priklausantis, bet nepriklauso

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;

Kada baitų intervalo langas — netinkamas įrankis

Būkite sąžiningi dėl šios ribos. Baitų intervalo perkrova daro prielaidą, kad konteineris jau visas atmintyje, o Count yra Integer, todėl vienas langas negali viršyti dviejų gigabaitų. Jei konteineris — 6 GB archyvas diske, ar atkeliauja per lizdą, kurio negalite persukti atgal, ši perkrova negali padėti, o viso dalyko įkėlimas į TBytes vien tam, kad adresuotumėte langą jame, nugalėtų prasmę. Kaip tik čia priklauso FPDF_FILEACCESS kelias, o straipsnis apie srautinį įkėlimą pagal poreikį parodo, kaip atverti poslinkiu pastumtą failo vaizdą kaip pasirinktinį dokumento šaltinį. Lygiai taip pat, jei įterptus baitus reikia transformuoti prieš PDFium juos pamatant, dekompresija, dešifravimas, atvyniojimo žingsnis, tada tikra kopija neišvengiama, ir Buffered = True transformuotam masyvui yra sąžiningas atsakymas. Baitų intervalo langas atsiperka lygiai vienoje formoje: nenutrūkstami, nepakeisti PDF baitai, jau rezidentiniai, žinomame poslinkyje

Jei vertinate tai žiūryklei, peržiūros skydeliui ar partijos priėmimo srautui, baitų intervalo perkrova ir srautinis įkėliklis — dvi iš įkėlimo strategijų, kurias PDFium Component siūlo kartu su failo, srauto ir žalios rodyklės įkėlimais. Visas API paviršius, licencijavimas ir Delphi bei C++Builder versijų palaikymas dokumentuoti PDFium Component produkto puslapyje