Aluetarkistusvirheillä Delphi PDF -kirjastoissa on maine vaikeasti paikannettavina ongelmina, koska ne eivät noudata johdonmukaista syöttökuviota. Sama asiakirja tuottaa ne yhdellä koneella ja toisella ei; sama koodipolku laukaisee poikkeuksen 3-sivuisessa tiedostossa, mutta suoritetaan virheettömästi 12-sivuisessa. Tämä epäjohdonmukaisuus jäljitetään lähes aina yhteen juurisyyhyn: PDF-sivuobjekteja ei tallenneta tiedoston järjestyksessä. Jos kirjasto rakentaa sisäisen sivutaulukkonsa skannaamalla objekteja peräkkäin sen sijaan, että se kävisi läpi luettelon määrittämän sivupuun, se luo indeksin, jonka kelvollinen alue ei vastaa kutsujien odotuksia, ja aluetarkistus nappaa tämän epäsuhdan pahimmalla mahdollisella hetkellä
Kuinka aluetarkistus toimii Delphissä
Kun {$R+}-kääntäjädirektiivi on aktiivinen (oletusarvo Debug-konfiguraatiossa), Delphi RTL vahvistaa jokaisen taulukon indeksin, merkkijonon alaindeksin ja luetellun tyypin sijoituksen ajon aikana. Rajojen ulkopuolella oleva pääsy herättää ERangeError-virheen sen sijaan, että se lukisi hiljaisesti viereistä muistia. Tämä käyttäytyminen on arvokasta: se tuo piilevät viat pintaan varhaisessa vaiheessa sen sijaan, että ne antaisivat niiden turmella tietorakenteen, joka epäonnistuu vasta sata riviä myöhemmin. Turhauttava osa on se, että poikkeus laukeaa pääsykohdassa, ei siinä pisteessä, jossa indeksi laskettiin väärin. Kun kutsupino näyttää syvälle sisennetyn menetelmän PDF-yksikössä, todellinen virhe on yleensä useita kehyksiä taaempana
Yhdistetyt totuusarvoehdot pahentavat tätä. Delphi arvioi and-lausekkeet vasemmalta oikealle oikosulkusemantiikalla, mutta oikosulku ohittaa arvioinnin vain, kun vasen puoli on False. Lauseke kuten:
if FDocStarted and (DestIndex < Length(PageArr)) and
(PageArr[DestIndex].PageObj <> nil) then
näyttää turvalliselta, mutta se suojaa alueen ulkopuoliselta indeksiltä vain, jos FDocStarted on True ja DestIndex ei ole negatiivinen. Tarkistus DestIndex < Length(PageArr) ei tee mitään, kun DestIndex on negatiivinen, koska negatiivisen kokonaisluvun vertaaminen ei-negatiiviseen pituuteen palauttaa True etumerkillisessä aritmetiikassa ja myöhempi taulukon käyttö laukaisee yhä aluevirheen. Rajatarkistuksen siirtäminen uloimpaan sijaintiin on oikea korjaus:
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]);
Tämä on mekaaninen korjaus. Se pysäyttää kaatumisen. Se ei selitä, miksi DestIndex sai alun perinkin arvon, joka on kelvollisen alueen ulkopuolella
Todellinen syy: objektien järjestys versus sivujen järjestys
ISO 32000-1 §7.7.3 määrittelee sivupuun Pages-solmujen puuna, jonka Kids-taulukot luettelevat sivuobjektit näyttöjärjestyksessä. Tiedosto tallentaa nämä objektit mihin tahansa siirtymiin (offsets), jotka kirjoittaja sattui valitsemaan; objekti numero 20 voi fyysisesti edeltää objektia numero 3 tavuvirrassa. Kirjasto, joka rakentaa sivuluettelonsa käymällä läpi ristiinviittaustaulukon (cross-reference table) objektinumerojärjestyksessä Kids-ketjun seuraamisen sijaan, tuottaa sekvenssin, joka eroaa siitä, mitä käyttäjä odottaa. Asiakirjoissa, joissa generaattori sattui kirjoittamaan sivut järjestyksessä, kaikki toimii. Asiakirjoissa, joissa näin ei tapahtunut, ero kirjaston sivunumeroinnin ja kutsujan sivunumeroinnin välillä tuottaa indeksejä, jotka putoavat PageArr-taulukon ulkopuolelle
Oikea lähestymistapa on aloittaa luettelosta (catalog), ratkaista /Pages epäsuora viittaus ja käydä Kids-taulukko läpi rekursiivisesti. Litteälle asiakirjalle, jossa ei ole väli-Pages-solmuja, läpikäynti on suoraviivaista:
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
// välisolmu: rekursio sen Kids-elementteihin
BuildPageIndexFromTree(Child.GetArray('/Kids'), PageArr);
end;
end;
end;
Tämän suorittamisen jälkeen PageArr[0] on ensimmäinen sivu, jonka katseluohjelma näyttäisi riippumatta siitä, missä kohtaa kyseinen objekti sijaitsee tavuvirrassa. Kutsujien välittämät indeksit, jotka olettavat näyttöjärjestyksen, yhdistyvät nyt oikein, ja aluevirheet loppuvat
Koodiin kovakoodatut kiertotavat pahentavat ongelmaa
Koodikannoissa, joissa juurisyytä ei koskaan tunnistettu, on yleistä löytää heuristisia paikkauksia: vaihda ensimmäinen ja viimeinen sivu, jos kokonaismäärä on 3, kierrä indeksiä tietystä generaattorista peräisin oleville asiakirjoille, sovella poikkeamaa (offset), kun ensimmäinen objektinumero ylittää kynnyksen. Jokainen näistä paikkauksista sopii täsmälleen niihin testitiedostoihin, jotka olivat käsillä silloin, kun se kirjoitettiin. Lisää erilainen PDF-lähde, ja yksi paikkauksista laukeaa väärään aikaan, tuottaen indeksin, joka on nyt tuplasti väärin: väärin, koska se laskettiin väärässä järjestyksessä olevasta taulukosta, ja väärin uudelleen, koska päälle sovellettiin soveltumatonta yhdistämistä. Aluetarkistus nappaa sen jossain alavirrassa, ja pinovedos (stack trace) ei osoita mihinkään hyödylliseen
Ainoa tuottava tie on poistaa jokainen heuristinen yhdistäminen ja korvata sivutaulukon rakentaminen asianmukaisella puun läpikäynnillä. Kun indeksit on rakennettu oikein, paikkauksia ei tarvita ja aluetarkistuksesta tulee voimavara esteen sijaan
Jos ylläpidät kirjastoa, joka osoittaa tätä kuviota, ota aluetarkistus väliaikaisesti käyttöön Release-koontiversiossa ja suorita se monipuolista PDF-korpusta vastaan: asiakirjoja, jotka on tuottanut Word, LaTeX, skannerin laiteohjelmisto (firmware), PDF-to-PDF -jakotyökalut. Tiedostot, jotka laukaisevat poikkeuksia, ovat niitä, joiden sivuobjektien järjestys poikkeaa koodisi olettamasta läpikäyntijärjestyksestä. Jokainen niistä on datapiste, ei erillinen virhe
Uudelle koodille, joka kutsuu Delphi PDF -kirjastoa, käytännön ohje on pitää kirjaston sivumäärää arvovaltaisena eikä koskaan välittää indeksiä, joka on johdettu aritmetiikasta ulkoisille tiedoille vahvistamatta ensin, että se osuu välille 0..PageCount - 1. HotPDF-komponentti paljastaa ratkaistun sivumäärän THotPDF.PageCount-ominaisuuden kautta BeginDoc-kutsun tai asiakirjan lataamisen jälkeen; tämä arvo heijastaa aina sivupuun läpikäyntiä ja on turvallista käyttää ylärajana mille tahansa indeksointiaritmetiikalle