Artikel Teknis

Kesalahan Range Check di Pustaka PDF Delphi: Penyebab Utama

Kesalahan range check di pustaka PDF Delphi mendapatkan reputasi sulit untuk dilacak karena tidak mengikuti pola input yang konsisten. Dokumen yang sama menghasilkannya di satu mesin dan tidak di mesin lain; jalur kode yang sama memicu exception pada file 3 halaman tetapi berjalan bersih pada file 12 halaman. Ketidakkonsistenan itu hampir selalu berakar pada satu penyebab: objek halaman PDF tidak disimpan dalam urutan file. Jika pustaka membangun array halaman internalnya dengan memindai objek secara berurutan alih-alih menelusuri pohon halaman yang dideklarasikan oleh catalog, maka pustaka membuat indeks yang rentang validnya tidak sesuai dengan yang diharapkan pemanggil, dan pemeriksaan range menangkap ketidaksesuaian itu pada saat yang paling tidak menguntungkan

Cara kerja pemeriksaan range di Delphi

Dengan direktif kompiler {$R+} aktif (default pada konfigurasi Debug), Delphi RTL memvalidasi setiap indeks array, subskrip string, dan penugasan enum pada saat runtime. Akses di luar batas memunculkan ERangeError alih-alih membaca memori yang berdekatan secara diam-diam. Perilaku itu berharga: ini memunculkan bug tersembunyi sejak dini alih-alih membiarkannya merusak struktur data yang baru gagal seratus baris kemudian. Bagian yang membuat frustrasi adalah bahwa exception terpicu di titik akses, bukan di titik di mana indeks dihitung secara salah. Ketika call stack menampilkan metode yang sangat bersarang di unit PDF, kesalahan sebenarnya biasanya beberapa frame ke belakang

Kondisi boolean majemuk membuat ini lebih buruk. Delphi mengevaluasi ekspresi and dari kiri ke kanan dengan semantik short-circuit, tetapi short-circuit hanya melewatkan evaluasi ketika sisi kiri adalah False. Ekspresi seperti:

if FDocStarted and (DestIndex < Length(PageArr)) and
   (PageArr[DestIndex].PageObj <> nil) then

terlihat aman, tetapi hanya melindungi terhadap indeks di luar rentang jika FDocStarted adalah True dan DestIndex tidak negatif. Pemeriksaan DestIndex < Length(PageArr) tidak berarti apa-apa ketika DestIndex negatif, karena membandingkan integer negatif dengan panjang non-negatif mengembalikan True dalam aritmetika bertanda dan akses array berikutnya tetap memicu kesalahan range. Memindahkan pemeriksaan batas ke posisi terluar adalah perbaikan yang benar:

if (DestIndex >= 0) and (DestIndex < Length(PageArr)) then
begin
  if FDocStarted and (PageArr[DestIndex].PageObj <> nil) then
    Result := PageArr[DestIndex].PageObj
  else
    Result := nil;
end
else
  raise ERangeError.CreateFmt(
    'Page index %d is out of range (0..%d)',
    [DestIndex, Length(PageArr) - 1]);

Ini adalah perbaikan mekanis. Ini menghentikan crash. Ini tidak menjelaskan mengapa DestIndex menerima nilai di luar rentang valid sejak awal

Penyebab sebenarnya: urutan objek versus urutan halaman

ISO 32000-1 §7.7.3 mendefinisikan pohon halaman sebagai pohon node Pages yang array Kids-nya mencantumkan objek halaman dalam urutan tampilan. File menyimpan objek-objek tersebut pada offset mana pun yang dipilih penulis; objek nomor 20 secara fisik dapat mendahului objek nomor 3 dalam aliran byte. Pustaka yang membangun daftar halamannya dengan mengiterasi tabel cross-reference dalam urutan nomor objek alih-alih mengikuti rantai Kids akan menghasilkan urutan yang berbeda dari yang diharapkan pengguna. Pada dokumen di mana generator kebetulan menulis halaman secara berurutan, semuanya berfungsi. Pada dokumen di mana tidak, ketidaksesuaian antara penomoran halaman pustaka dan penomoran halaman pemanggil menghasilkan indeks yang berada di luar PageArr

Pendekatan yang benar adalah memulai dari catalog, me-resolve referensi indirect /Pages, dan menelusuri array Kids secara rekursif. Untuk dokumen datar tanpa node Pages perantara, traversal cukup mudah:

procedure BuildPageIndexFromTree(
  const KidsArray: THPDFArray;
  var PageArr: TPageObjArray);
var
  i, Idx: Integer;
  Child: THPDFObject;
  ChildType: string;
begin
  for i := 0 to KidsArray.Count - 1 do
  begin
    Child := KidsArray.GetIndirectObject(i);
    if Child = nil then
      Continue;
    ChildType := Child.GetNameValue('/Type');
    if ChildType = 'Page' then
    begin
      Idx := Length(PageArr);
      SetLength(PageArr, Idx + 1);
      PageArr[Idx].PageObj := Child;
    end
    else if ChildType = 'Pages' then
    begin
      // intermediate node: recurse into its Kids
      BuildPageIndexFromTree(Child.GetArray('/Kids'), PageArr);
    end;
  end;
end;

Setelah ini berjalan, PageArr[0] adalah halaman pertama yang akan ditampilkan viewer, terlepas dari di mana objek tersebut berada dalam aliran byte. Indeks yang dilewatkan oleh pemanggil yang mengasumsikan urutan tampilan kini memetakan dengan benar, dan kesalahan range berhenti

Solusi sementara yang dikodekan keras memperparah masalah

Dalam codebase di mana penyebab akar tidak pernah diidentifikasi, umum ditemukan patch heuristik: tukar halaman pertama dan terakhir jika jumlah total sama dengan 3, rotasi indeks untuk dokumen dari generator tertentu, terapkan offset ketika nomor objek pertama melebihi ambang batas. Setiap patch itu cocok persis dengan set file uji yang tersedia saat ditulis. Tambahkan sumber PDF yang berbeda dan salah satu patch terpicu pada waktu yang salah, menghasilkan indeks yang kini salah ganda: salah karena dihitung dari array yang tidak terurut, dan salah lagi karena pemetaan yang tidak berlaku diterapkan di atasnya. Pemeriksa range menangkapnya di suatu tempat di hilir dan stack trace tidak menunjuk ke mana pun yang berguna

Satu-satunya jalur produktif adalah menghapus setiap pemetaan heuristik dan mengganti konstruksi array halaman dengan penelusuran pohon yang benar. Setelah indeks benar secara konstruksi, tidak ada patch yang diperlukan dan pemeriksa range menjadi aset, bukan hambatan

Jika Anda memelihara pustaka yang menunjukkan pola ini, aktifkan pemeriksaan range dalam build Release sementara dan jalankan terhadap korpus PDF yang beragam: dokumen yang dibuat oleh Word, oleh LaTeX, oleh firmware scanner, oleh utilitas split PDF-to-PDF. File yang memicu exception adalah file yang urutan objek halamannya berbeda dari urutan traversal yang diasumsikan kode Anda. Setiap satu adalah data poin, bukan bug terpisah

Untuk kode baru yang memanggil pustaka PDF Delphi, saran praktisnya adalah memperlakukan jumlah halaman pustaka sebagai otoritatif dan tidak pernah melewatkan indeks yang berasal dari aritmetika pada data eksternal tanpa terlebih dahulu mengonfirmasi nilainya berada dalam 0..PageCount - 1. Komponen HotPDF mengekspos jumlah halaman yang diselesaikan melalui THotPDF.PageCount setelah BeginDoc atau setelah memuat dokumen; nilai tersebut selalu mencerminkan traversal pohon halaman dan aman digunakan sebagai batas atas untuk aritmetika indeks apa pun