Delphi PDF kütüphanelerindeki aralık kontrolü hataları, tutarlı bir girdi deseni izlemedikleri için tespit edilmelerinin zor olmasıyla ün salmıştır. Aynı belge bu hataları bir makinede üretirken diğerinde üretmez; aynı kod yolu 3 sayfalık bir dosyada istisna fırlatırken 12 sayfalık bir dosyada sorunsuz çalışır. Bu tutarsızlığın kök nedeni neredeyse her zaman tek bir şeye dayanır: PDF sayfa nesneleri dosya sırasında depolanmaz. Kütüphane kendi iç sayfa dizisini, katalog tarafından bildirilen sayfa ağacında gezinmek yerine nesneleri sırayla tarayarak oluşturursa, geçerli aralığı çağıranların beklediğiyle eşleşmeyen bir dizin oluşturur ve aralık denetimi bu uyumsuzluğu mümkün olan en kötü anda yakalar
Delphi'de aralık kontrolü nasıl çalışır?
{$R+} derleyici yönergesi etkinken (Hata Ayıklama yapılandırmasında varsayılandır), Delphi RTL çalışma zamanında her dizi dizinini, dize alt simgesini ve numaralandırılmış atamayı doğrular. Sınır dışı bir erişim, bitişik belleği sessizce okumak yerine ERangeError hatası fırlatır. Bu davranış değerlidir: gizli hataları yüz satır sonra başarısız olan bir veri yapısını bozmasına izin vermek yerine erkenden yüzeye çıkarır. Sinir bozucu olan kısım, istisnanın dizinin yanlış hesaplandığı noktada değil, erişim noktasında fırlatılmasıdır. Çağrı yığını bir PDF biriminde derinden iç içe geçmiş bir yöntemi gösterdiğinde, asıl hata genellikle birkaç kare geridedir
Bileşik boolean koşulları bunu daha da kötüleştirir. Delphi, and ifadelerini kısa devre semantiği ile soldan sağa değerlendirir, ancak kısa devre yapma işlemi yalnızca sol taraf False olduğunda değerlendirmeyi atlar. Şunun gibi bir ifade:
if FDocStarted and (DestIndex < Length(PageArr)) and
(PageArr[DestIndex].PageObj <> nil) then
güvenli görünür, ancak yalnızca FDocStarted True ise ve DestIndex negatif değilse sınır dışı bir dizine karşı koruma sağlar. DestIndex < Length(PageArr) kontrolü, DestIndex negatif olduğunda hiçbir şey yapmaz, çünkü işaretli aritmetikte negatif bir tam sayıyı negatif olmayan bir uzunlukla karşılaştırmak True döndürür ve sonraki dizi erişimi yine de aralık hatası fırlatır. Sınır kontrolünü en dış konuma taşımak doğru çözümdür:
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]);
Bu mekanik bir düzeltmedir. Çökmeyi durdurur. Ancak DestIndex değerinin en başta neden geçerli aralığın dışında bir değer aldığını açıklamaz
Asıl neden: nesne sırası ve sayfa sırası karşılaştırması
ISO 32000-1 §7.7.3, sayfa ağacını, Kids dizilerinin sayfa nesnelerini görüntüleme sırasına göre listelediği bir Pages düğümleri ağacı olarak tanımlar. Dosya, bu nesneleri yazarın seçtiği herhangi bir uzaklıkta depolar; 20 numaralı nesne bayt akışında 3 numaralı nesneden fiziksel olarak önce gelebilir. Sayfa listesini Kids zincirini izlemek yerine çapraz başvuru (cross-reference) tablosunu nesne numarası sırasına göre yineleyerek oluşturan bir kütüphane, kullanıcının beklediğinden sapan bir sıra üretecektir. Oluşturucunun sayfaları sırayla yazdığı belgelerde her şey çalışır. Öyle yapmadığı belgelerde ise, kütüphanenin sayfa numaralandırması ile çağıranın sayfa numaralandırması arasındaki tutarsızlık PageArr dizisinin dışına düşen dizinler üretir
Doğru yaklaşım, katalogdan başlamak, /Pages dolaylı referansını çözümlemek ve Kids dizisinde özyinelemeli olarak gezinmektir. Ara Pages düğümleri olmayan düz bir belge için gezinme basittir:
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;
Bu çalıştıktan sonra, bu nesnenin bayt akışında nerede bulunduğuna bakılmaksızın PageArr[0], bir görüntüleyicinin göstereceği ilk sayfa olur. Görüntüleme sırasını varsayan çağıranlar tarafından iletilen dizinler artık doğru bir şekilde eşlenir ve aralık hataları durur
Sabit kodlu geçici çözümler sorunu büyütür
Kök nedenin hiçbir zaman belirlenemediği kod tabanlarında, buluşsal (heuristic) yamalar bulmak yaygındır: toplam sayı 3'e eşitse ilk ve son sayfayı değiştir, belirli bir oluşturucudan gelen belgeler için dizini döndür, ilk nesne numarası bir eşiği aştığında bir kaydırma (offset) uygula. Bu yamaların her biri, yazıldığı sırada elde bulunan test dosyaları kümesine tam olarak uyar. Farklı bir PDF kaynağı eklediğinizde yasalardan biri yanlış zamanda tetiklenir ve artık iki kat yanlış olan bir dizin üretir: yanlıştır çünkü sırasız bir diziden hesaplanmıştır ve yine yanlıştır çünkü üstüne uygulanamaz bir eşleme uygulanmıştır. Aralık denetleyicisi bunu daha aşağılarda bir yerde yakalar ve yığın izlemesi (stack trace) yararlı hiçbir yeri göstermez
Üretken tek yol, her buluşsal eşlemeyi kaldırmak ve sayfa dizisi yapısını uygun bir ağaç gezinmesiyle değiştirmektir. Dizinler yapısal olarak doğru olduğunda, hiçbir yamaya gerek kalmaz ve aralık denetleyicisi bir engel olmaktan çıkıp bir avantaj dönüşür
Eğer bu kalıbı sergileyen bir kütüphanenin bakımını yapıyorsanız, geçici olarak bir Release derlemesinde aralık kontrolünü etkinleştirin ve çeşitli bir PDF kümesine karşı çalıştırın: Word, LaTeX, tarayıcı bellenimi (firmware) ve PDF'den PDF'e bölme (split) araçları tarafından üretilen belgeler. İstisnaları tetikleyen dosyalar, sayfa nesne sırası kodunuzun varsaydığı gezinme sırasından sapan dosyalardır. Her biri ayrı bir hata (bug) değil, bir veri noktasıdır
Bir Delphi PDF kütüphanesini çağıran yeni kodlar için pratik tavsiye, kütüphanenin sayfa sayısını yetkili olarak kabul etmek ve dış veriler üzerindeki aritmetikten türetilen bir dizini, önce 0..PageCount - 1 aralığına düştüğünü onaylamadan asla geçmemektir. HotPDF bileşeni, BeginDoc çağrısından veya bir belge yüklendikten sonra çözümlenmiş sayfa sayısını THotPDF.PageCount aracılığıyla sunar; bu değer her zaman sayfalar ağacı gezinmesini yansıtır ve herhangi bir dizin aritmetiği için üst sınır olarak kullanmak güvenlidir