Greške provere opsega u Delphi PDF bibliotekama imaju reputaciju teških za otkrivanje jer ne prate dosledan obrazac unosa. Isti dokument ih proizvodi na jednoj mašini, a ne na drugoj; ista putanja koda pokreće izuzetak na datoteci od 3 stranice, ali radi čisto na onoj od 12 stranica. Ta nedoslednost se gotovo uvek svodi na jedan osnovni uzrok: objekti PDF stranica se ne čuvaju prema redosledu u datoteci. Ako biblioteka gradi svoj unutrašnji niz stranica uzastopnim skeniranjem objekata umesto da prolazi kroz stablo stranica deklarisano u katalogu, ona konstruiše indeks čiji se važeći opseg ne podudara sa onim što pozivaoci očekuju, a provera opsega hvata to neslaganje u najgorem mogućem trenutku
Kako funkcioniše provera opsega u Delphiju
Sa aktivnom {$R+} direktivom prevodioca (podrazumevano u Debug konfiguraciji), Delphi RTL proverava svaki indeks niza, indeks niza znakova (string) i nabrojanu (enumerated) dodelu tokom izvršavanja. Pristup van granica pokreće ERangeError umesto da tiho čita susednu memoriju. Takvo ponašanje je dragoceno: rano otkriva skrivene greške umesto da im dozvoli da oštete strukturu podataka koja bi otkazala tek stotinu linija kasnije. Frustrirajući deo je to što se izuzetak pokreće na mestu pristupa, a ne na mestu gde je indeks pogrešno izračunat. Kada pozivni stek (call stack) prikaže duboko ugneždenu metodu u PDF jedinici, stvarna greška je obično nekoliko okvira unazad
Složeni logički uslovi (compound boolean conditions) ovo čine još gorim. Delphi izračunava and izraze s leva na desno sa semantikom kratkog spoja (short-circuit), ali kratki spoj preskače izračunavanje samo kada je leva strana False. Izraz kao što je:
if FDocStarted and (DestIndex < Length(PageArr)) and
(PageArr[DestIndex].PageObj <> nil) then
izgleda bezbedno, ali štiti od indeksa van opsega samo ako je FDocStarted jednako True i DestIndex nije negativan. Provera DestIndex < Length(PageArr) ne radi ništa kada je DestIndex negativan, jer poređenje negativnog celog broja sa nenegativnom dužinom vraća True u aritmetici sa predznakom, a naknadni pristup nizu i dalje pokreće grešku provere opsega. Premeštanje provere granica na krajnju spoljašnju poziciju je ispravno rešenje:
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]);
Ovo je mehaničko rešenje. Ono zaustavlja pad programa. Međutim, ne objašnjava zašto je DestIndex uopšte dobio vrednost izvan važećeg opsega
Pravi uzrok: redosled objekata naspram redosleda stranica
Standard ISO 32000-1 §7.7.3 definiše stablo stranica kao stablo Pages čvorova čiji Kids nizovi navode objekte stranica u redosledu prikaza. Datoteka čuva te objekte na onim ofsetima koje je kreator izabrao; objekat broj 20 može fizički prethoditi objektu broj 3 u toku bajtova (byte stream). Biblioteka koja gradi svoju listu stranica iteracijom tabele unakrsnih referenci (cross-reference table) prema redosledu brojeva objekata umesto praćenja lanca Kids proizvešće niz koji se razlikuje od onoga što korisnik očekuje. Kod dokumenata gde je generator slučajno zapisao stranice po redu, sve funkcioniše. Kod dokumenata gde to nije slučaj, neslaganje između numeracije stranica biblioteke i numeracije pozivaoca proizvodi indekse koji padaju izvan niza PageArr
Ispravan pristup je započeti od kataloga, razrešiti /Pages indirektnu referencu i rekurzivno proći kroz niz Kids. Za ravan dokument bez posrednih Pages čvorova, pretraga je pravolinijska:
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;
Nakon što se ovo izvrši, PageArr[0] je prva stranica koju bi pregledač prikazao, bez obzira na to gde se taj objekat nalazi u toku bajtova. Indeksi prosleđeni od strane pozivaoca koji pretpostavljaju redosled prikaza sada se ispravno preslikavaju, a greške provere opsega prestaju
Hardkodovana rešenja (workarounds) pogoršavaju problem
U bazama koda gde pravi uzrok nikada nije identifikovan, uobičajeno je pronaći heurističke zakrpe: zameniti prvu i poslednju stranicu ako je ukupan broj jednak 3, rotirati indeks za dokumente iz specifičnog generatora, primeniti ofset kada broj prvog objekta premaši prag. Svaka od tih zakrpa tačno odgovara skupu testnih datoteka koje su bile dostupne kada je napisana. Dodajte drugačiji PDF izvor i jedna od zakrpa se pokreće u pogrešno vreme, proizvodeći indeks koji je sada dvostruko pogrešan: pogrešan jer je izračunat iz niza koji nije po redu i ponovo pogrešan jer je na vrhu primenjeno neodgovarajuće preslikavanje. Proveravač opsega ga hvata negde nizvodno (downstream) i trag steka ne ukazuje ni na šta korisno
Jedini produktivan put je ukloniti sva heuristička preslikavanja i zameniti konstrukciju niza stranica pravilnim prolaskom kroz stablo. Kada su indeksi ispravni po konstrukciji, zakrpe nisu potrebne, a proveravač opsega postaje prednost umesto prepreke
Ako održavate biblioteku koja ispoljava ovaj obrazac, privremeno omogućite proveru opsega u Release verziji i pokrenite je protiv raznovrsnog korpusa PDF-ova: dokumenata koje proizvode Word, LaTeX, firmver skenera, uslužni programi za deljenje (split) PDF-a u PDF. Datoteke koje pokreću izuzetke su one čiji redosled objekata stranica odstupa od redosleda pretrage koji vaš kod pretpostavlja. Svaka od njih je tačka podataka, a ne zasebna greška
Za novi kod koji poziva Delphi PDF biblioteku, praktičan savet je da se broj stranica biblioteke tretira kao merodavan i nikada se ne prosleđuje indeks izveden iz aritmetike na spoljnim podacima, a da se prethodno ne potvrdi da spada u opseg 0..PageCount - 1. Komponenta HotPDF izlaže rešen broj stranica kroz svojstvo THotPDF.PageCount nakon poziva BeginDoc ili nakon učitavanja dokumenta; ta vrednost uvek odražava pretragu stabla stranica i bezbedna je za korišćenje kao gornja granica za bilo kakvu indeksnu aritmetiku