Greške provjere raspona u Delphi PDF bibliotekama zarađuju reputaciju po tome što ih je teško otkriti jer ne slijede dosljedan obrazac unosa. Isti ih dokument proizvodi na jednom stroju, a ne na drugom; ista staza koda pokreće iznimku na datoteci od 3 stranice, ali radi čisto na datoteci od 12 stranica. Ta nedosljednost gotovo se uvijek svodi na jedan temeljni uzrok: PDF objekti stranica nisu pohranjeni prema redoslijedu datoteka. Ako biblioteka izgradi svoj interni niz stranica skeniranjem objekata sekvencijalno (sequentially), umjesto hodanjem kroz stablo stranica (page tree) deklarirano u katalogu, ona konstruira indeks čiji važeći raspon ne odgovara onome što pozivatelji očekuju, a provjera raspona hvata tu nepodudarnost u najgorem mogućem trenutku
Kako funkcionira provjera raspona u Delphiju
Kada je direktiva kompajlera {$R+} aktivna (zadano u Debug konfiguraciji), Delphi RTL validira svaki indeks niza (array index), pretplatu za niz znakova (string subscript) i pobrojanu dodjelu (enumerated assignment) tijekom izvođenja (runtime). Pristup izvan granica pokreće (raises) ERangeError umjesto da nečujno čita susjednu memoriju. Takvo ponašanje je dragocjeno: rano izlazi na vidjelo prikrivene greške umjesto da dopusti da oštete strukturu podataka koja bi zakazala tek stotinu linija kasnije. Frustrirajući dio je taj što iznimka puca na mjestu pristupa, a ne na mjestu gdje je indeks netočno izračunat. Kada stog poziva (call stack) prikazuje duboko ugniježđenu metodu (nested method) u PDF jedinici, stvarna greška obično je nekoliko okvira (frames) unatrag
Složeni logički uvjeti pogoršavaju stvari. Delphi procjenjuje and izraze slijeva nadesno s logikom kratkog spoja (short-circuit semantics), ali kratki spoj preskače evaluaciju samo kada je lijeva strana False. Izraz poput:
if FDocStarted and (DestIndex < Length(PageArr)) and
(PageArr[DestIndex].PageObj <> nil) then
izgleda sigurno, ali štiti od indeksa izvan raspona samo ako je FDocStarted jednako True i DestIndex nije negativan. Provjera DestIndex < Length(PageArr) ne čini ništa kada je DestIndex negativan, jer usporedba negativnog cijelog broja (integer) s nenegativnom duljinom vraća True u predznačnoj aritmetici (signed arithmetic), a naknadni pristup nizu i dalje pokreće pogrešku raspona. Premještanje provjere granica (bounds check) na krajnji vanjski položaj je ispravno rješ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čki popravak. To zaustavlja rušenje (crash). Ne objašnjava zašto je DestIndex prvenstveno primio vrijednost izvan važećeg raspona
Stvarni uzrok: redoslijed objekata naspram redoslijeda stranica
ISO 32000-1 §7.7.3 definira stablo stranica (page tree) kao stablo Pages čvorova (nodes) čiji nizovi Kids ispisuju objekte stranice u redoslijedu prikaza (display order). Datoteka pohranjuje te objekte u kojim god pomacima (offsets) je pisac (writer) odabrao; objekt broj 20 može fizički prethoditi objektu broj 3 u toku bajtova (byte stream). Biblioteka koja gradi svoj popis stranica prelaženjem (iterating) unakrsne referentne tablice redoslijedom brojeva objekata, a ne praćenjem Kids lanca, proizvest će slijed koji odstupa od onoga što korisnik očekuje. Kod dokumenata gdje se generator slučajno dogodio da stranice zapiše redoslijedom, sve funkcionira. U dokumentima u kojima to nije uspjelo, razlika između označavanja stranica (page numbering) u biblioteci i označavanja stranica pozivatelja stvara indekse (indices) koji ispadaju izvan PageArr
Ispravan pristup je započeti iz kataloga, razriješiti /Pages indirektnu referencu i hodati rekurzivno kroz niz Kids. Za ravan dokument (flat document) bez srednjih Pages čvorova, prelazak (traversal) je jednostavan:
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 pokrene, PageArr[0] je prva stranica koju bi preglednik prikazao, bez obzira na to gdje se taj objekt nalazi u toku bajtova. Indeksi proslijeđeni od strane pozivatelja (callers) koji pretpostavljaju ispravan redoslijed mapiranja zaslona (display order) sada se ispravno podudaraju, i pogreške raspona (range errors) prestaju
Fiksna (hard-coded) zaobilazna rješenja otežavaju problem
U bazama koda (codebases) u kojima temeljni uzrok nikad nije identificiran, uobičajeno je pronaći heurističke zakrpe (patches): zamijeni prvu i zadnju stranicu ako je ukupan broj jednak 3, rotiraj indeks za dokumente od specifičnog generatora, primijeni pomak (offset) kada prvi broj objekta premaši prag. Svaka od tih zakrpa odgovara točno onom skupu testnih datoteka koje su bile pri ruci kada je napisana. Dodajte drugačiji PDF izvor i jedna od zakrpa se pokreće u krivo vrijeme, proizvodeći indeks koji je sada dvostruko netočan: netočan jer je izračunat iz neporedanog niza, i ponovno netočan jer je neprimjenjivo preslikavanje primijenjeno povrh toga. Alat za provjeru raspona hvata ga negdje nizvodno, a ispis stoga (stack trace) ne ukazuje na ništa korisno
Jedini produktivni put je ukloniti sva heuristička mapiranja (heuristic mapping) i zamijeniti konstrukciju niza stranica s ispravnim hodanjem kroz stablo (tree walk). Jednom kada se indeksi isprave kroz konstrukciju, nisu potrebne nikakve zakrpe i program za provjeru raspona postaje prednost umjesto prepreke
Ako održavate biblioteku koja pokazuje ovaj obrazac, privremeno omogućite provjeru raspona (range checking) u verziji (Release build) i pokrenite ju na raznolikom korpusu PDF-ova: dokumenti kreirani pomoću Worda, LaTeX-a, na razini ugrađene programske opreme (firmware) skenera te uslužnim programima za razdvajanje iz PDF u PDF (split utilities). Datoteke koje izazivaju iznimke one su čiji redoslijed objekata na stranici odstupa od redoslijeda prolaženja koji pretpostavlja vaš kôd. Svaki od njih je podatkovna točka (data point), a ne zasebna greška (bug)
Za novi kod koji poziva Delphi PDF biblioteku, praktičan savjet je tretirati broj stranica u biblioteci kao autoritativan i nikada ne prosljeđivati indeks izveden iz aritmetike na vanjskim podacima, a da prethodno niste potvrdili da spada unutar granica 0..PageCount - 1. HotPDF komponenta izlaže (exposes) riješeni broj stranica putem THotPDF.PageCount nakon BeginDoc ili nakon učitavanja dokumenta; ta vrijednost uvijek odražava prolazak kroz stablo stranica i sigurno ju je koristiti kao gornju granicu (upper bound) za svaku aritmetiku indeksa