A tartomány-ellenőrzési hibák (range check errors) a Delphi PDF könyvtárakban arról híresek, hogy nehéz őket behatárolni, mivel nem követnek következetes bemeneti mintát. Ugyanaz a dokumentum előidézi őket az egyik gépen, de egy másikon nem; ugyanaz a kódútvonal kivételt dob egy 3 oldalas fájlnál, de hibátlanul lefut egy 12 oldalasnál. Ez a következetlenség szinte mindig egyetlen gyökérokra vezethető vissza: a PDF oldalobjektumok nincsenek a fájlban sorrendben tárolva. Ha a könyvtár a belső oldaltömbjét az objektumok szekvenciális szkennelésével építi fel, ahelyett, hogy végigjárná a katalógus által deklarált oldalfát, akkor olyan indexet hoz létre, amelynek érvényes tartománya nem egyezik a hívók által elvárttal, és a tartomány-ellenőrzés a lehető legrosszabb pillanatban kapja el ezt az eltérést
Hogyan működik a tartomány-ellenőrzés a Delphiben
Az {$R+} fordítói direktíva aktiválásával (ami a Debug konfiguráció alapértelmezése) a Delphi RTL futásidőben érvényesít minden tömbindexet, karakterlánc-alszkriptet és felsorolt értékadást. Egy határokon kívüli hozzáférés ERangeError-t vált ki ahelyett, hogy csendben kiolvasná a szomszédos memóriát. Ez a viselkedés értékes: korán felszínre hozza a látens hibákat ahelyett, hogy hagyná, hogy egy adatstruktúra megsérüljön, és csak száz sorral később okozzon hibát. A frusztráló rész az, hogy a kivétel a hozzáférés helyén (access site) lép fel, nem pedig azon a ponton, ahol az indexet helytelenül kiszámították. Amikor a hívási verem egy mélyen beágyazott metódust mutat egy PDF egységben, a valódi hiba általában néhány kerettel hátrébb van
Az összetett logikai feltételek még rontanak a helyzeten. A Delphi az and kifejezéseket balról jobbra értékeli ki rövidzárlatos (short-circuit) szemantikával, de a rövidzárlat csak akkor hagyja ki a kiértékelést, ha a bal oldal False. Egy ilyen kifejezés:
if FDocStarted and (DestIndex < Length(PageArr)) and
(PageArr[DestIndex].PageObj <> nil) then
biztonságosnak tűnik, de csak akkor véd a tartományon kívüli index ellen, ha az FDocStarted értéke True és a DestIndex nem negatív. A DestIndex < Length(PageArr) ellenőrzés semmit sem ér, ha a DestIndex negatív, mivel egy negatív egész szám és egy nem negatív hossz összehasonlítása előjeles aritmetikában True-t ad, és a későbbi tömbhozzáférés továbbra is kiváltja a tartományhibát. A határellenőrzés legkülső pozícióba helyezése a helyes megoldás:
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]);
Ez a mechanikus javítás. Megállítja az összeomlást. Nem magyarázza meg, hogy a DestIndex miért kapott érvényes tartományon kívüli értéket elsősorban
A valódi ok: objektumsorrend kontra oldalsorrend
Az ISO 32000-1 §7.7.3 az oldalfát olyan Pages csomópontok fájaként definiálja, amelyek Kids tömbjei megjelenítési sorrendben sorolják fel az oldalobjektumokat. A fájl azokat az objektumokat tetszőleges eltolással tárolja, ahogy azt az író választotta; a 20-as objektumszám fizikailag megelőzheti a 3-as objektumszámot a bájtsorozatban. Az a könyvtár, amely az oldallistáját a kereszthivatkozási tábla (cross-reference table) objektumszám szerinti bejárásával építi fel a Kids lánc követése helyett, olyan sorozatot fog eredményezni, amely eltér attól, amit a felhasználó elvár. Azokon a dokumentumokon, ahol a generátor történetesen sorrendben írta az oldalakat, minden működik. Azokon a dokumentumokon, ahol nem, a könyvtár oldalszámozása és a hívó oldalszámozása közötti eltérés olyan indexeket hoz létre, amelyek a PageArr-on kívül esnek
A helyes megközelítés a katalógusból (catalog) való indulás, a /Pages indirekt hivatkozás feloldása, és a Kids tömb rekurzív végigjárása. Egy lapos dokumentum esetében, amelynek nincsenek közbenső Pages csomópontjai, a bejárás egyszerű:
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;
Ennek lefutása után a PageArr[0] lesz az első oldal, amelyet a nézegető (viewer) megjelenítene, függetlenül attól, hogy ez az objektum hol helyezkedik el a bájtsorozatban. A megjelenítési sorrendet feltételező hívók által átadott indexek most már helyesen vannak leképezve, és a tartományhibák megszűnnek
A kódba égetett áthidaló megoldások rontanak a problémán
Olyan kódbázisokban, ahol soha nem azonosították a gyökérokot, gyakoriak a heurisztikus javítások: cserélje meg az első és az utolsó oldalt, ha a teljes szám 3; forgassa el az indexet bizonyos generátorból származó dokumentumokhoz; alkalmazzon eltolást, ha az első objektum száma meghalad egy küszöböt. Ezen javítások mindegyike pontosan ahhoz a tesztfájlkészlethez illeszkedik, amelyik kéznél volt, amikor írták. Adjon hozzá egy másik PDF forrást, és az egyik javítás rossz időben aktiválódik, olyan indexet eredményezve, amely most kétszeresen rossz: rossz, mert egy rendezetlen tömbből számították ki, és újra rossz, mert egy nem alkalmazható leképezést is ráhúztak. A tartomány-ellenőrző valahol lejjebb elkapja, és a veremkövetés (stack trace) semmi hasznosra nem mutat
Az egyetlen produktív út, ha eltávolítunk minden heurisztikus leképezést, és lecseréljük az oldaltömb felépítését egy megfelelő fabejárásra (tree walk). Ha az indexek a felépítésükből adódóan helyesek, nincsenek szükségletek javításokra, és a tartomány-ellenőrző akadály helyett értékké (asset) válik
Ha egy olyan könyvtárat tart karban, amely ezt a mintát mutatja, engedélyezze a tartomány-ellenőrzést egy Release build-ben ideiglenesen, és futtassa azt egy változatos PDF korpuszon: Word által, LaTeX által, szkenner firmware által, PDF-ből PDF-be osztó segédprogramok által előállított dokumentumokon. A kivételt kiváltó fájlok azok, amelyek oldalobjektumainak sorrendje eltér a kódja által feltételezett bejárási sorrendtől. Mindegyik egy-egy adatpont, nem pedig külön hiba
Azoknál az új kódoknál, amelyek egy Delphi PDF könyvtárat hívnak, az a gyakorlati tanács, hogy a könyvtár oldalszámát tekintse mérvadónak, és soha ne adjon át külső adatokon végzett aritmetikából származó indexet anélkül, hogy először megerősítené, hogy az a 0..PageCount - 1 tartományba esik. A HotPDF component a BeginDoc vagy egy dokumentum betöltése után a feloldott oldalszámot a THotPDF.PageCount tulajdonságon keresztül teszi közzé; ez az érték mindig tükrözi az oldalak fájának bejárását, és biztonságosan használható bármely index aritmetika felső határaként