Teknik Makale

Gömülü PDF'ler için PDFium Byte Range Yükleme

Taşma denetimi kapalıyken 2000000000 + 2000000000, her iki operand Delphi Integer değeriyse -294.967.296 olarak değerlenir. O sayı, bir kapsayıcı ayrıştırıcısı bir kayıt başlığından bir uzaklık ve bir uzunluk okuyup gömülü PDF'in tamponun içine sığıp sığmadığını görmek için bunları topladığı anda bilmece olmaktan çıkar. Toplam negatife sarar, Length(Data) ile karşılaştırma geçer ve bir sonraki satır, dizinin sonundan kabaca iki gigabayt ötedeki bir baytın adresini alır. Daha büyük bir tamponun ortasından bir PDF yüklemek, asla toplama yapmayan bir aralık denetimi ve belge açıldığında baytların sahibinin kim olduğuna dair net bir yanıt ister

Bu yazı, hâlihazırda RAM'de duran, başka bir şeyin içinde bilinen bir uzaklıkta oturan belgeleri kapsar. Bellekte tutulamayacak kadar büyük belgeler başka bir sorundur; PDFium VCL ile büyük PDF'leri talep üzerine akıtma yazısında anlatıldığı gibi blokların diskten talep üzerine çekilmesiyle ele alınır. Son bölüm çizgiyi çeker

Bilinen bir uzaklıktaki gömülü PDF

Bir posta deposu, mesaj gövdelerini ve ekleri tek bir kayıtta tutar. Bir arşiv kapsayıcısı bir manifesti, birkaç görseli ve bir PDF'i arka arkaya ekler. Özel bir kablo protokolü, belgeyi uzunluk önekli bir başlığın arkasına çerçeveler. Kayıt 40 MB olsun, PDF 1.182.336 baytta başlayıp 312 KB sürsün. Alışılmış kod, dilim kesmek için Copy(Data, Index, Count) çağırır ve o dilimi, kendi tamponuna bir kez daha kopyalayan bir yükleyiciye uzatır. Kapsayıcıyı hesaba katarsak aynı 312 KB artık üç kez vardır ve bir posta kutusu taraması bunu her mesaj için yineler

PDFium Component pencereye doğrudan seslenir. TPdf, LoadDocument(const Data: TBytes; Index, Count: Integer; Buffered: Boolean = True) bildirir; Index sıfır tabanlı bir bayt konumu, Count bir bayt uzunluğudur. Metot her iki sayıyı doğrular, tek bir işaretçi hesaplar ve belgeyi FPDF_LoadMemDocument64 üzerinden açan LoadDocument(Data: Pointer; Size: NativeUInt; Buffered: Boolean) işaretçi overload'una devreder. Tüm dizi biçimi olan LoadDocument(const Data: TBytes; Buffered: Boolean), bayt aralığı biçimine 0 ve Length(Data) ile tek satırlık bir çağrıdır; yani her TBytes yüklemesi aynı doğrulamadan geçer

Eski çift-kopyalama yolunu (Delphi'de bir kapsayıcı içindeki gömülü PDF için) PDFium Component bayt-aralığı LoadDocument aşırı yüklemesiyle (pencereyi yerinde adresler) karşılaştıran karşılaştırma şeması
Copy ile dilimleyip sonra yüklemek, aynı 312 KB'lik pencereyi iki kez kopyalar; bayt-aralığı çağrısı onu yerinde adresler ve Buffered, hiçbir şeyin kopyalanıp kopyalanmayacağını seçer

Kodda dilim tamamen ortadan kalkar. LocateEmbeddedPdf, kendi kapsayıcı ayrıştırıcınızın yerini tutar

uses
  System.SysUtils, System.IOUtils, PDFium;

procedure PreviewEmbedded(const RecordFile: string);
var
  Frame: TBytes;           // tüm kapsayıcı kayıt, onlarca megabayt
  Offset, Size: Integer;
  Pdf: TPdf;
begin
  Frame := TFile.ReadAllBytes(RecordFile);
  LocateEmbeddedPdf(Frame, Offset, Size);   // sizin ayrıştırıcınız, bileşenin parçası değil

  Pdf := TPdf.Create(nil);
  try
    // Copy(Frame, Offset, Size) yok: pencere yerinde adreslenir
    Pdf.LoadDocument(Frame, Offset, Size);  // Buffered varsayılan olarak True
    Frame := nil;  // burada sorun yok: yalnızca PDF penceresi kopyalandı
    Writeln('Pages: ', Pdf.PageCount);
  finally
    Pdf.Free;      // belgeyi kapatır
  end;
end;

Küresel EnableV8Engine hâlâ False iken ve PDFium kütüphanesi henüz yüklenmemişken, bellek yükleyicileri gelen baytları XFA işaretleri için tarar ve bulurlarsa V8 yetenekli DLL yüklensin diye EnableV8Engine'i açarlar. Bayt aralığı çağrısında o tarama yalnızca pencereyi görür. Aynı kayıtta başka bir yerde duran bir manifest ya da ikinci bir belge onu tetikleyemez

Index artı Count sınır kontrolünü neden taşırıyor?

Çünkü Index ile Count ikisi de 32 bitlik işaretli Integer değeridir ve iki büyük pozitifin toplamı güvenilir biçimde büyük pozitif bir sonuç üretmez. Uzaklık ve uzunluk bir dosya başlığından geliyorsa, ikisini de bir saldırgan ya da bozuk bir kayıt yönetir. Aynı tuzak, o sayıları önce hesaplayan kendi kapsayıcı ayrıştırıcınızda da pusuda bekler

// Kendi kapsayıcı ayrıştırıcınız için bir yardımcı (öneri; bileşenin parçası değil)
function IsValidWindow(const Data: TBytes; Index, Count: Integer): Boolean;
begin
  // YANLIŞ: Index + Count Integer olarak değerlendirilir ve negatife sarabilir
  //   Result := (Index >= 0) and (Index + Count <= Length(Data));

  // DOĞRU: işaretleri reddet, Index'i kendi başına sınırla, sonra Count'ı
  // kalana karşı karşılaştır; tek aritmetik, saramayacak bir çıkarmadır
  Result := (Index >= 0) and (Count >= 0) and
            (Index <= Length(Data)) and
            (Count <= Length(Data) - Index);
end;

Başarısız durumu elden geçirin. Index = 2000000000 ve Count = 2000000000 iken gerçek toplam dört milyardır ama 32 bit işaretli aritmetik onu -294.967.296 olarak saklar. O değer herhangi bir Length(Data)'dan rahatça küçüktür; yanlış denetim geçer ve @Data[Index], iki gigabaytlık bir uzunluk eklenmiş şekilde dizinin iyice dışına işaret eder. Çökme iyi sonuçtur. Kötüsü, PDFium'un orada tesadüfen duran süreç belleğini ayrıştırmasıdır

Doğru sıralama asla toplamaz. Negatif değerler, hiçbir şey indekslenmeden önce reddedilir. Index sonra tek başına Length(Data) ile sınırlanır; bu, Length(Data) - Index'in negatif olmayan bir Integer olmasını garanti eder ve ancak o zaman Count o kalana karşı karşılaştırılır. Delphi and'i varsayılan olarak kısa devre mantığıyla değerlendirir ({$B-}); sonraki her terim, öncekileri sağlanmadan koşmaz. Güvenlik ağı olarak {$Q+}'a yaslanmayın: sürüm derlemeleri taşma denetimi kapalı olarak gönderilir ve açıkken bile, bir bellek güvenliği deliğini bir doğrulama yordamının ortasından kaçan bir EIntOverflow'a takas etmiş olursunuz

Taşan Index artı Count sınır kontrolünü, PDFium bayt-aralığı yüklemesini Delphi'de bellek açısından güvenli tutan yalnızca-çıkarma doğrulama sırasıyla karşılaştıran akış şeması
Önce eklemek MaxInt'i aşar ve korumayı etkisiz bırakır; işaretleri reddetmek ile terimleri sınırlamak, tek bir sarmasız çıkarmadan önce her ara değeri temsil edilebilir tutar

Bayt aralığı LoadDocument aynı dört testi aynı sırada koşar ve başarısız kuralı adıyla söyleyen bir mesajla EPdfError fırlatır: "PDF byte range index cannot be negative", "PDF byte range count cannot be negative", "PDF byte range index exceeds data length" ve "PDF byte range exceeds data length". Her birini dört baytlık bir diziyle yeniden üretebilirsiniz: Index = 5, Count = 0 üçüncü mesajı tetikler, Index = 2, Count = 3 dördüncüyü ve her reddin ardından Pdf.Active hâlâ False'tur. Güvenilmeyen uzunlukların yerel sınıra bu şekilde muamelesi, Delphi'de PDFium VCL ABI'sini ve bellek güvenliğini sertleştirme yazısında daha geniş ele alınır

Sıfır uzunluklu bir pencere hâlâ yasal bir çağrıdır

Boş bir pencere bir nil işaretçi geçirir; çünkü @Data[Index], doğrulamanın kabul ettiği her Index için yasal bir ifade değildir. Count = 0 ile Index = Length(Data), tamponun kuyruğunda iyi biçimli boş bir penceredir ve boş bir TBytes, sıfır numaralı elemanı bulunmayan bir dizide Index = 0 verir. Her iki durumda da adres almak sonun ötesini okur ya da bir nil dinamik diziyi referans eder ve aralık denetimi açıksa Delphi, yükleyici daha koşmadan ERangeError fırlatır

Bu yüzden bayt aralığı overload'u dallanır: Count = 0 nil verir, başka her sayı @Data[Index] verir. İşaretçi overload'u nil'i yalnızca sıfır boyutla birlikte kabul eder, PDFium sonra boş bir tamponu açmayı reddeder ve çağrı, PDFium'un son hata metnini taşıyan sıradan bir EPdfError ile, PDFium hiçbiri bildirmediyse "Cannot load PDF document" ile biter. Sıfır baytlık bir ek bildiren bir başlık, öteki her kötü girdiyle aynı yakalanabilir istisnayı alır. Bunu ayrıştırıcınızda, mesajda kayıt numarasıyla reddetmek, destek günlüğünde yine de daha iyi okunur

Buffered, baytların sahibini kararlaştırır

Buffered, sonuçları çağrının ötesine taşan tek parametredir. Buffered = True iken bileşen, seçili pencereyi ve yalnızca pencereyi, yüklemeden önce kendi tamponuna kopyalar. 40 MB'lık örnekte 312 KB kopyalanır ve kapsayıcıya dokunulmaz. LoadDocument döner dönmez kapsayıcıyı serbest bırakabilir, yeniden kullanabilir ya da üzerine yazabilirsiniz

Buffered = False iken @Data[Index] doğrudan FPDF_LoadMemDocument64'e gider ve PDFium, belge açık kaldığı sürece o işaretçiden okumayı sürdürür. Arkasındaki tüm TBytes ödünç verilmiş olur. Active := False set edene ya da TPdf'i serbest bırakana kadar canlı ve değişmemiş kalmalıdır. Önemli olan pencere değil tüm dizidir: bir Delphi dinamik dizisi tek blok olarak referans sayılır, yani kodunuzun herhangi bir yerinde son referansın düşmesi, PDFium'un hâlâ okuduğu belleği serbest bırakır. SetLength de aynı derecede ölümcüldür; bir yeniden ayırma bloğu taşıyabilir

İki Delphi ayrıntısı, bunu göründüğünden daha kolay bozabilir. Dinamik dizilerin, dizelerin aksine kopyala-yaz yoktur; Other := FFrame; Other[0] := 0;, PDFium'un ayrıştırdığı baytların ta kendisine yazar. Ve ağ kodunuzun sonraki çerçeve için yeniden kullandığı bir alım tamponu, açık belgenin altında üzerine yazılır. Pdf.LoadDocument(ReadRecord(Id), Offset, Size, False) gibi bir fonksiyon sonucunu doğrudan geçmek, diziyi çağıran yordamla ölen gizli bir geçiciye bırakır; belge ise yaşamaya devam eder

Delphi'de gömülü bir PDF'yi bayt aralığından yüklerken iki Buffered modunun zaman çizelgesi şeması: yalnızca PDF penceresini kopyalayıp kapsayıcıyı hemen serbest bırakmak ile belge kapatılana dek tüm destek TBytes'ı ödünç almak
Buffered = True yalnızca pencereyi kopyalar, böylece kapsayıcı tek kullanımlıktır; Buffered = False ise bütün destek dizisini, belge kapatılana dek ödünç bırakır

İş parçacığından geçen, yeniden kullanılabilir bir tamponun arkasında duran ya da onu yükleyen yordamdan uzun yaşayan her şey için Buffered = True tutun. Ek maliyet, PDF'in kendi boyutunda bir kopyadır ve karşılığında çağrı döner dönmez 40 MB'lık bir kaydı düşürme özgürlüğü satın alırsınız. Ödünç alma, yalnızca kapsayıcının hâlihazırda bütün oturum boyunca tuttuğunuz uzun ömürlü bir alan olduğu zaman hak eder; kullanıcının eki arasında sayfa çevirirken arşiv kaydını açık tutan bir görüntüleyici gibi. O durumda sahipliği tek bir sınıfta açık hale getirin

type
  TFrameSession = class
  private
    FFrame: TBytes;   // FPdf'te açık bir belge varken destek deposunun sahibi
    FPdf: TPdf;
  public
    constructor Create(const AFrame: TBytes);
    destructor Destroy; override;
    procedure OpenEmbedded(Offset, Size: Integer);
  end;

constructor TFrameSession.Create(const AFrame: TBytes);
begin
  inherited Create;
  FFrame := AFrame;   // diziyi paylaşır; artık başkası yazamaz
  FPdf := TPdf.Create(nil);
end;

procedure TFrameSession.OpenEmbedded(Offset, Size: Integer);
begin
  FPdf.Active := False;                            // önce varsa önceki pencereyi kapat
  FPdf.LoadDocument(FFrame, Offset, Size, False);  // FFrame bu belgeden uzun yaşamalı
end;

destructor TFrameSession.Destroy;
begin
  FPdf.Active := False;  // 1. ödünç verileni serbest bırak
  FPdf.Free;
  FFrame := nil;         // 2. depolama ancak şimdi gidebilir
  inherited;
end;

Kurucu, çağıranın dizisini kopyalamak yerine paylaşır; yani çağıranın artık ona yazmayı bırakması gerekir. Bunu vaat edemiyorsanız kurucuya Copy(AFrame) geçirin ve oturum başına bir kez, yükleme başına değil, yapılan tek kopyaya geri dönersiniz

Pencerenin %PDF- ile başladığını denetlemek

Birkaç bayt sapmış bir pencere ince başarısızlıktır, çünkü belge yine de açılabilir. ISO 32000-1 §7.5.4, her çapraz referans girdisini dosyanın başından bir bayt uzaklığı olarak tanımlar ve §7.5.2 %PDF- başlığını başa koyar. Ayrıştırıcınız bir öneki yanlış sayar ve pencere sekiz bayt erken başlarsa, o uzaklıklar sekiz bayt yanlıştır. PDFium başlığı ilk 1024 bayt içinde arar ve baştaki çöpü telafi eder; geç başlayan ya da erken biten bir pencere, onu bulduğu nesne işaretlerinden çapraz referans tablosunu yeniden kurmaya itebilir. Her iki durumda da sayfa sayısı doğru görünür. Ayrıştırıcınızdaki uzaklık hâlâ yanlıştır ve o kaydı okuyan bir sonraki okuyucu daha az affedici olabilir

LoadDocument(const Data: TBytes; Index, Count: Integer; const Options: TPdfLoadOptions; out Report: TPdfLoadReport; Buffered: Boolean = True) overload'u bunu görünür kılar. Report.HeaderOffset, %PDF-'in pencerenin ilk 1024 baytı içinde bulunduğu yerdir; bulunamadıysa -1. Report.UsedRecovery, PDFium çapraz referans tablosunu geçerli kabul etmediyse True'dur. Rapordaki tüm uzaklıklar kapsayıcıdan değil, pencerenin ilk baytından sayılır. Çok kısa bir Count, sondaki %%EOF'u keser ve TPdfLoadOptions.Default(plmStrict), RequireFinalEndOfFileMarker'ı açar; yani katı mod o penceri tümüyle reddeder

var
  Options: TPdfLoadOptions;
  Report: TPdfLoadReport;
begin
  Options := TPdfLoadOptions.Default;   // plmCompatible
  Pdf.LoadDocument(Frame, Offset, Size, Options, Report, True);

  // LogWarning sizin günlük yordamınız
  if Report.HeaderOffset > 0 then
    LogWarning(Format('Window starts %d bytes before %%PDF-, check Offset',
      [Report.HeaderOffset]))
  else if Report.HeaderOffset < 0 then
    LogWarning('No %PDF- header in the first 1024 bytes of the window');

  if Report.UsedRecovery then
    LogWarning('Cross-reference table was rebuilt, check Offset and Size');
end;

O alanların arkasındaki denetimin, ödünç yüklemeler için önemli bir maliyeti vardır. Pencere Options.AuditByteLimit'e (varsayılan 256 MB) sığıyorsa denetim pencerenin geçici bir kopyasını okur; yani Buffered = False bir raporla birleştiğinde yükleme artık kopyasız değildir. AuditByteLimit'i 0 yapmak denetimi ve kopyayı atlar, HeaderOffset'i -1'de bırakır. Kapsayıcı ayrıştırıcısını geliştirirken ve kayıtların yeni kaynaklarına karşı denetimli biçimi koşun; uzaklıklara güvenildikten sonra düz overload'a geçin. Raporun geri kalanı PDFium yük raporuyla PDF yükleme başarısızlıklarını yakalama yazısında ele alınır

Byte aralığının yanlış araç olduğu zaman

Bayt aralığı overload'u, tüm kapsayıcının hâlihazırda bellekte olduğunu varsayar ve Count bir Integer olduğundan tek bir pencere MaxInt baytı aşamaz. Diskteki 6 GB'lık bir arşiv ya da geri sarılamayan bir soket o kalıba sığmaz; içindeki bir pencereyi adreslemek için her şeyi TBytes'e okumak ise amacı yener. O durumlar için LoadCustomDocument(AStream: TStream; AOwnsStream: Boolean), PDFium'un blokları aramalı bir akıştan talep üzerine çekmesine izin verir. Her Seek'e PDF'in başlangıç uzaklığını ekleyen küçük bir TStream torununuz, gömülü belgeyi sıfır konumunda başlıyormuş gibi gösterir; talep üzerine akıtma yazısı bunu gösterir

Öteki uyumsuzluk, henüz düz bir PDF olmayan içeriktedir. Gömülü baytlar sıkıştırılmışsa, kapsayıcı tarafından şifrelenmişse ya da başka bir kodlamaya sarılmışsa, dönüştürülmüş bir kopyadan kaçınamazsınız ve o yeni diziyi varsayılan Buffered = True ile yüklemek düz sayılan cevaptır. Bayt aralığı tek biçimde parayı öder: bitişik, değişmemiş PDF baytları, hâlihazırda yerleşik, ayrıştırıcınızın toplama yapmadan doğruladığı bir uzaklıkta

Bayt aralığı çağrısı, aynı TPdf bileşeninde dosya, akış, ham işaretçi ve talep üzerine yüklemanın yanında durur. PDFium Component ürün sayfası, desteklenen Delphi ve C++Builder sürümlerini, lisanslama seçeneklerini ve yükleme API'sinin geri kalanını listeler