Teknik Makale

Hasarlı PDF Xref Tablolarını Yeniden İnşa: Delphi Kurtarma Taraması

Bir PDF çapraz-referans tablosu kullanılamaz olduğunda, düzeltme onu tamamen yoksayıp dosya gövdesinden yeniden inşa etmektir. PDFlibPas Delphi PDF Library, gördüğü her gerçek dolaylı nesne başlığını kaydeden tek geçişlik bir token tarayıcısıyla bunu yapar, ardından trailer sözlüğünü kurtarır ve yeniden inşa edilmiş tabloyu normal yükleyiciye teslim eder

Bir PDF hasar gördüğünde önce ne bozulur

Çapraz-referans tablosu bir PDF'in en kırılgan parçasıdır, çünkü mutlak bayt ofsetlerini saklayan tek parçadır. ISO 32000-1 §7.5.4, bu girdileri dosyanın başlangıcından itibaren on basamaklı ofsetler olarak tanımlar ve §7.5.5, startxref anahtar kelimesini sonda, tablonun kendisine işaret eder biçimde yerleştirir. Bu sayıların her biri, baytları kaydıran herhangi bir düzenleme tarafından geçersiz kılınır. Metin modunda çalışan ve CRLF'yi çeviren bir FTP oturumu, kesilmiş bir indirme, paylaşılan bir sürücüde bozulan bir sektör, artımlı bir güncellemeyi doğru yazmadan ekleyen bir toplu araç: bunların hepsi nesne verisini mükemmel derecede okunabilir bırakır ve indeksi çöpe işaret eder biçimde bırakır

Bu, "dosya hasarlı ve onarılıyor" iletisinin bu kadar yaygın olmasının nedenidir. Baytlar neredeyse her zaman hâlâ oradadır. Kayıp olan haritadır. Yeniden inşa bu yüzden kaybolmuş verinin adli kurtarımı değildir, gövdeden türetilebilen bir indeksin yeniden inşasıdır ve kullanıcıların beklediğinden çok daha sık başarılı olur, çünkü pahalı içerik — sayfa ağaçları ve fontlar ve görüntüler — dokunulmamıştır

N 0 obj için tarama neden yanlış eşleşmeler bulur?

Saf bir yeniden inşa, ham baytları "tamsayı, tamsayı, obj" deseni için arar ve her isabeti kaydeder. Çok fazla bulur. PDF bir konteyner biçimidir ve bir dosyanın üç bölgesi nesne dilbilgisine karşı opaktır: yorumlar (§7.2), dizeler (§7.3.4) ve akış verisi (§7.3.8). Bunların herhangi biri, tam olarak bir nesne başlığı gibi okunan baytlar içerebilir ve hiçbiri bir nesne başlığı değildir. Bir literal dizedeki bir başlık, arta kalan bir hata ayıklama yorumu ya da iki megabayt Flate ya da DCT çıktısı, hepsi seve seve 99 0 obj gibi görünen bir şey üretir

const
  Trap: AnsiString =
    '4 0 obj'#10 +
    '(a caption that mentions 88 0 obj)'#10 +   // literal string, not an object
    'endobj'#10 +
    '% 77 0 obj left over from a debug dump'#10 +  // comment, not an object
    '5 0 obj'#10 +
    '<< /Length 2097152 >>'#10 +
    'stream'#10 +
    { two MiB of compressed bytes that contain the byte sequence
      99 0 obj and, further along, a complete endstream }
    'endstream'#10 +
    'endobj'#10;

Her yanlış girdi iki kez maliyete mal olur. Yeniden inşa edilmiş tabloyu var olmayan bir nesne numarasıyla kirletir ve dosyada daha sonra görünen aynı numaraya sahip gerçek bir nesneyi gölgeleyebilir. PDFlibPas bu yüzden desen-eşleştirme yapmaz. Tokenize eder, ki bu, imlecin altındaki baytların kod mu yoksa yük mü olduğunu her zaman bildiği anlamına gelir ve yük hiç yorumlanmadan atlanır

64 KiB'lik bloklar üzerinde tek geçişlik bir durum makinesi

PDFlibPas, tüm dosyayı tam olarak bir kez, 64 KiB'lik bloklarda, ISO 32000-1 §7.2'nin token kurallarına ve §7.3.10'un dolaylı nesne sözdizimine dayanan bir durum makinesiyle tarar. Bir token, boşlukta ya da sınırlayıcı karakterlerden birinde biter ve bir nesne başlığı yalnızca pozitif bir nesne numarası, negatif olmayan bir üretim numarası ve tek başına bir obj anahtar kelimesinin tam sırası görüldüğünde kaydedilir. Kaydedilen ofset, bir çapraz-referans girdisinin işaret etmesi gereken şey olan nesne numarası token'ının başlangıcıdır, obj anahtar kelimesinin konumu değil

function RebuildIsWhiteSpace(Value: Byte): Boolean;
begin
  Result := (Value = 0) or (Value = 9) or (Value = 10) or
            (Value = 12) or (Value = 13) or (Value = 32);
end;

function RebuildIsDelimiter(Value: Byte): Boolean;
begin
  Result := (Value = Ord('(')) or (Value = Ord(')')) or
            (Value = Ord('<')) or (Value = Ord('>')) or
            (Value = Ord('[')) or (Value = Ord(']')) or
            (Value = Ord('{')) or (Value = Ord('}')) or
            (Value = Ord('/')) or (Value = Ord('%'));
end;

Önemli ayrıntı, token durumunun ve dize durumunun bir blok sınırında hayatta kalmasıdır. 65536-bayt çizgisine yayılan bir başlık hâlâ tanınır, çünkü kısmi token, bekleyen tamsayı çifti ve dize-içi bayraklar hepsi bir sonraki bloğa taşınır. Tamponlar sabittir: tarama için 64 KiB, mümkün olan en uzun token için 32 bayt ve dosyayla birlikte büyüyen tek diziler, dosya boyutuna değil gerçek nesne sayısına orantılı olan nesne numarası, üretim numarası ve 64-bit ofset listeleridir. Pratikte tarama, tüm doküman üzerinde sıralı okumalar ve en fazla iki açık arama yayınlar; bu, doğrudan erişim birleştirme ve bölme üzerine yazıda tartışılan çok-yüz-megabaytlık girdilerde onu uygulanabilir kılan şeydir

Bir akışın endstream'de bittiğine neden güvenilemez?

Çünkü akış verisi keyfi baytlardır ve keyfi baytlar tesadüfen endstream hecelerini oluşturabilir. stream anahtar kelimesinden sonra başlayan bir akış, gerçekten bitene kadar opak veri olarak atlanmalıdır, ama kapanış anahtar kelimesinin ilk görülüşü yalnızca bir adaydır. PDFlibPas bunu doğrulama gerektirerek çözer: bir endstream token'ı, ancak bir sonraki boşluk-olmayan token, §7.3.8'in bir akış nesnesi etrafında gerektirdiği sıra olan tek başına bir endobj olduğunda akışın gerçek sonu olarak kabul edilir. Sıkıştırılmış veri içindeki tesadüfi bir isabetin neredeyse hiçbir zaman bu devamı yoktur, bu yüzden tarayıcı akışın içinde kalır ve devam eder. İki daha küçük kural aynı ölçüde önemlidir. stream anahtar kelimesi yalnızca tek başına bir anahtar kelime olduğunda akış durumuna girer, bu yüzden bir sözlükteki /stream gibi bir isim nesnesi asla onu tetiklemez. Ve bir obj ya da trailer token'ı, yalnızca token 32-bayt sınırını aşmadığında ve bir eğik çizgiyle başlamadığında onurlandırılır. Bu iki koruma olmadan yanlış anahtar isimlerine sahip bir kaynak sözlüğü taramayı raydan çıkarmaya yeterdi; bu, güvenilmeyen PDF'i güvenle ayrıştırma üzerine notlarda ele alınan tam olarak düşman girdi sınıfıdır

Trailer sözlüğünün gerçek sonunu bulmak

Nesneleri kurtarmak işin yalnızca yarısıdır, çünkü yükleyicinin /Root'u bulmak için hâlâ bir trailer'a ihtiyacı vardır. PDFlibPas, tarama sırasında bulunan son 64 trailer anahtar kelimesi konumunu hatırlar ve bunları geriye doğru, en yeniden başlayarak doğrular, bu yüzden en yeni kullanılabilir trailer kazanır ve bir sözlük takip etmeyen başıboş bir anahtar kelime basitçe doğrulamayı geçemez ve bir önceki adaya düşer. Her aday 1 MiB'lik bir sınırla okunur ve sözlük sonu, literal dize kaçışları, onaltılık dizeler ve yorumlarla birlikte iç içe << ve >> derinliğini izleyerek bulunur

// A naive reader that stops at the first '>>' truncates this trailer,
// and a fixed 2048-byte window can cut it in half on a large one
'trailer'#10 +
'<< /Size 5 /Root 1 0 R' +
'   /Custom << /Text (value >> preserved) >> >>'#10

Derinlik izleme akademik değildir. /Encrypt'i kaybeden kesilmiş bir trailer, kurtarılabilir şifreli bir dokümanı açılamaz bir dokümana dönüştürür ve /Info'yu ya da özel bir alt sözlüğü kaybetmek, aşağı akış sistemin bağımlı olabileceği meta veriyi sessizce atar. Dosya şifreliyse, kurtarılan trailer normal kimlik bilgisi yolunun çalışmasını sağlayan şeydir ve yeniden deneme anlambilimi, şifreli doküman yükleme üzerine yazıda anlatılan aynı anlambilimdir

Yeniden inşa size neyi geri veremez

Yeniden inşa en iyi çabadır ve sınırları hakkında dürüst olmak, onu göndermenin bir parçasıdır. Üç durum kesinlikle başarısız olur. Nesne akışlarının içinde paketlenmiş nesneler (§7.5.7), bir bayt taramasına ayrı ayrı görünmez, bu yüzden bir konteyner hayatta kalırsa ama çapraz-referans akışı (§7.5.8) kalmazsa, tuttuğu nesneler yeniden inşa tarafından indekslenmez. Gövdesi gerçekten bozulmuş, yalnızca yanlış indekslenmemiş bir dosya, içerikleri artık ayrıştırılamayan başlıklar üretecektir. Ve kurtarılabilir bir trailer anahtar kelimesi ve okunabilir bir katalog olmayan bir dosyanın, kaç nesne başlığı bulunmuş olursa olsun, bir doküman ağacını bağlayacak hiçbir şeyi yoktur

Tekrar eden nesne numaraları ilginç orta durumdur. Artımlı biçimde güncellenmiş bir dosya, meşru olarak aynı nesne numarasının birkaç üretimini içerir ve hayatta kalan çapraz-referans zinciri hangisinin geçerli olduğunun tek kaydıdır. Bir yeniden inşanın o zinciri yoktur, bu yüzden gördüğü her başlığı dosya sırasında kaydeder ve sonradan nesne numarasına göre çözer. Genellikle daha sonraki revizyon kazanır, ki bu genellikle doğrudur, ama güncellenip sonra kısmen geri alınmış bir doküman, orijinal xref'in tanımladığından incelikli biçimde farklı geri gelebilir. Doğrusallaştırılmış dosyalar diğer yönden aynı uyarıyı taşır: ilk-sayfa düzeni ve ipucu tabloları, indeks yeniden oluşturulduktan sonra anlamsızdır, bu yüzden onarılmış bir dosya sıradan, doğrusallaştırılmamış bir doküman olarak ele alınmalıdır

var
  Pdf: TPDFlib;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.LoadFromFile('truncated-invoice.pdf', '') = 1 then
    begin
      if Pdf.GetDocumentRepaired = 1 then
        LogWarning('xref was unusable; the table was reconstructed');
      if Pdf.PageCount > 0 then
        Pdf.SaveToFile('recovered-invoice.pdf');   // writes a clean xref
    end;
  finally
    Pdf.Free;
  end;
end;

Geri dönüş otomatiktir: PDFlibPas, çapraz-referans zinciri okunamadığında ve her kullanımda-olan girdinin ofset sıfır iddia ettiği zaman — ki bu, yazılmış ama hiç doldurulmamış bir tablonun imzasıdır — ham taramayı çalıştırır. GetDocumentRepaired, o yol çalıştığında 1 döndürür ve bunu yoksaymak yerine günlüğe kaydetmeye değer, çünkü yeniden inşa üzerinden yüklenen bir doküman, sanki hiçbir şey olmamış gibi bir pipeline'da bırakılmak yerine temiz bir dosyaya yeniden kaydedilmelidir. Onu kaydetmek, her aşağı akış tüketici için mümkün olan en ucuz düzeltme olan taze, tutarlı bir çapraz-referans tablosu yazar

Burada gösterilen yeniden inşa yolu, GetDocumentRepaired bayrağı ve streaming yükleyici, bu blogda başka yerde ele alınan ayrıştırma, rendering ve imzalama API'lerinin yanı sıra PDFlibPas Delphi PDF Library'nin bir parçasıdır