Range check-fejl i Delphi PDF-biblioteker har et ry for at være svære at indkredse, fordi de ikke følger et konsekvent inputmønster. Det samme dokument producerer dem på én maskine og ikke en anden; den samme kodesti udløser undtagelsen på en fil med 3 sider, men kører problemfrit på en med 12 sider. Denne inkonsekvens kan næsten altid spores tilbage til en enkelt grundlæggende årsag: PDF-sideobjekter gemmes ikke i filrækkefølge. Hvis biblioteket bygger sit interne side-array ved at scanne objekter sekventielt i stedet for at gennemgå sidetræet, der er deklareret i kataloget, konstruerer det et indeks, hvis gyldige interval ikke stemmer overens med, hvad kaldere forventer, og range checking fanger denne uoverensstemmelse på det værst tænkelige tidspunkt
Hvordan range checking fungerer i Delphi
Når compilerdirektivet {$R+} er aktivt (standarden i Debug-konfigurationen), validerer Delphi RTL hvert array-indeks, streng-subscript og opregnet tildeling på kørselstidspunktet. En adgang uden for grænserne rejser ERangeError i stedet for stille at læse tilstødende hukommelse. Den adfærd er værdifuld: Den afslører latente fejl tidligt i stedet for at lade dem ødelægge en datastruktur, der først fejler hundrede linjer senere. Den frustrerende del er, at undtagelsen udløses på adgangsstedet, ikke på det punkt, hvor indekset blev beregnet forkert. Når kaldstakken viser en dybt indlejret metode i en PDF-enhed, er den egentlige fejl normalt flere rammer tilbage
Sammensatte booleske betingelser gør dette værre. Delphi evaluerer and-udtryk fra venstre mod højre med short-circuit-semantik, men short-circuiting springer kun evaluering over, når venstre side er False. Et udtryk som:
if FDocStarted and (DestIndex < Length(PageArr)) and
(PageArr[DestIndex].PageObj <> nil) then
ser sikkert ud, men det beskytter kun mod et indeks uden for intervallet, hvis FDocStarted er True, og DestIndex er ikke-negativt. Kontrollen DestIndex < Length(PageArr) gør ingenting, når DestIndex er negativt, fordi sammenligning af et negativt heltal med en ikke-negativ længde returnerer True i aritmetik med fortegn, og den efterfølgende array-adgang udløser stadig range-fejlen. At flytte grænsekontrollen til den yderste position er den korrekte løsning:
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]);
Dette er den mekaniske rettelse. Den stopper nedbruddet. Den forklarer ikke, hvorfor DestIndex overhovedet modtog en værdi uden for det gyldige interval
Den egentlige årsag: Objektrækkefølge versus siderækkefølge
ISO 32000-1 §7.7.3 definerer sidetræet som et træ af Pages-knuder, hvis Kids-arrays viser sideobjekter i visningsrækkefølge. Filen gemmer disse objekter ved de forskydninger, forfatteren tilfældigvis valgte; objekt nummer 20 kan fysisk gå forud for objekt nummer 3 i bytestrømmen. Et bibliotek, der bygger sin sideliste ved at gennemløbe krydsreferencetabellen i objektnummerrækkefølge i stedet for at følge Kids-kæden, vil producere en sekvens, der afviger fra, hvad brugeren forventer. I dokumenter, hvor generatoren tilfældigvis har skrevet sider i rækkefølge, fungerer alt. I dokumenter, hvor det ikke skete, producerer uoverensstemmelsen mellem bibliotekets sidenummerering og kalderens sidenummerering indekser, der falder uden for PageArr
Den korrekte tilgang er at starte fra kataloget, løse den indirekte reference /Pages og gennemgå Kids-arrayet rekursivt. For et fladt dokument uden mellemliggende Pages-knuder er gennemløbet ligetil:
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;
Når dette er kørt, er PageArr[0] den første side, en fremviser ville vise, uanset hvor det objekt befinder sig i bytestrømmen. Indekser videregivet af kaldere, der antager visningsrækkefølge, tilknyttes nu korrekt, og range-fejlene stopper
Hårdkodede løsninger forværrer problemet
I kodebaser, hvor den grundlæggende årsag aldrig blev identificeret, er det almindeligt at finde heuristiske patches: Byt første og sidste side, hvis det samlede antal er 3, rotér indekset for dokumenter fra en bestemt generator, anvend en forskydning, når det første objektnummer overstiger en tærskel. Hver af disse patches passer nøjagtigt til det sæt testfiler, der var ved hånden, da den blev skrevet. Tilføj en anden PDF-kilde, og en af disse patches udløses på det forkerte tidspunkt og producerer et indeks, der nu er dobbelt forkert: forkert, fordi det blev beregnet fra et array uden for rækkefølge, og forkert igen, fordi en ubrugelig tilknytning blev anvendt oveni. Range-kontrollen fanger det et sted længere nede, og staksporingen peger ikke på noget brugbart
Den eneste produktive vej er at fjerne hver heuristisk tilknytning og erstatte konstruktionen af side-arrayet med en ordentlig gennemgang af træet. Når indekserne er korrekte ved konstruktion, er ingen patches nødvendige, og range-kontrollen bliver et aktiv i stedet for en hindring
Hvis du vedligeholder et bibliotek, der udviser dette mønster, skal du aktivere range checking midlertidigt i en Release-build og køre det mod et forskelligartet korpus af PDF'er: dokumenter produceret af Word, af LaTeX, af scanner-firmware, af PDF-til-PDF-opdelingsværktøjer. De filer, der udløser undtagelser, er dem, hvis sideobjektrækkefølge afviger fra den gennemløbsrækkefølge, din kode antager. Hver enkelt er et datapunkt, ikke en separat fejl
For ny kode, der kalder et Delphi PDF-bibliotek, er det praktiske råd at behandle bibliotekets sideantal som autoritativt og aldrig videregive et indeks afledt af aritmetik på eksterne data uden først at bekræfte, at det falder inden for 0..PageCount - 1. HotPDF-komponenten eksponerer det løste sideantal via THotPDF.PageCount efter BeginDoc eller efter indlæsning af et dokument; denne værdi afspejler altid sidetræets gennemløb og er sikker at bruge som den øvre grænse for al indeksaritmetik