Områdekontrollfeil i Delphi PDF-biblioteker har et rykte på seg for å være vanskelige å finne fordi de ikke følger et konsekvent inndatamønster. Det samme dokumentet produserer dem på én maskin, men ikke på en annen; den samme kodestien utløser unntaket på en fil med 3 sider, men kjører feilfritt på en med 12 sider. Denne inkonsekvensen kan nesten alltid spores tilbake til én enkelt hovedårsak: PDF-sideobjekter lagres ikke i filrekkefølge. Hvis biblioteket bygger sin interne side-array ved å skanne objekter sekvensielt fremfor å følge sidetreet deklarert av katalogen, konstruerer det en indeks der det gyldige området ikke samsvarer med det innringeren forventer, og områdekontrollen fanger opp dette avviket på verst tenkelige tidspunkt
Hvordan områdekontroll fungerer i Delphi
Når kompilatordirektivet {$R+} er aktivt (standard i Debug-konfigurasjonen), validerer Delphi RTL hver array-indeks, streng-subskript og oppregnede tilordning ved kjøretid. En tilgang utenfor grensene utløser ERangeError i stedet for at man i stillhet leser tilstøtende minne. Den atferden er verdifull: den bringer latente feil til overflaten tidlig i stedet for å la dem korrumpere en datastruktur som bare svikter hundre linjer senere. Den frustrerende delen er at unntaket utløses ved tilgangspunktet, ikke der indeksen ble beregnet feil. Når anropsstakken viser en dypt nøstet metode i en PDF-enhet, er den virkelige feilen vanligvis flere nivåer tilbake
Sammensatte boolske betingelser gjør dette verre. Delphi evaluerer and-uttrykk fra venstre til høyre med kortslutningssemantikk, men kortslutning hopper bare over evaluering når venstre side er False. Et uttrykk som:
if FDocStarted and (DestIndex < Length(PageArr)) and
(PageArr[DestIndex].PageObj <> nil) then
ser trygt ut, men det beskytter bare mot en indeks utenfor grensene hvis FDocStarted er True og DestIndex er ikke-negativ. Sjekken DestIndex < Length(PageArr) gjør ingenting når DestIndex er negativ, fordi å sammenligne et negativt heltall med en ikke-negativ lengde returnerer True i aritmetikk med fortegn, og den påfølgende array-tilgangen utløser fortsatt områdefeilen. Å flytte grensekontrollen til den ytterste posisjonen er den korrekte løsningen:
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 løsningen. Den stopper krasjet. Den forklarer ikke hvorfor DestIndex fikk en verdi utenfor det gyldige området i utgangspunktet
Den virkelige årsaken: objektrekkefølge kontra siderekkefølge
ISO 32000-1 §7.7.3 definerer sidetreet som et tre med Pages-noder der Kids-arrayene lister opp sideobjekter i visningsrekkefølge. Filen lagrer disse objektene på de forskyvningene (offsets) skribenten tilfeldigvis valgte; objektnummer 20 kan fysisk komme før objektnummer 3 i bytestrømmen. Et bibliotek som bygger sidelisten sin ved å iterere over kryssreferansetabellen i objektnummer-rekkefølge i stedet for å følge Kids-kjeden, vil produsere en sekvens som avviker fra det brukeren forventer. På dokumenter der generatoren tilfeldigvis skrev sidene i rekkefølge, fungerer alt. På dokumenter der den ikke gjorde det, produserer uoverensstemmelsen mellom bibliotekets sidenummerering og innringerens sidenummerering indekser som faller utenfor PageArr
Den riktige tilnærmingen er å starte fra katalogen, løse den indirekte /Pages-referansen og gå gjennom Kids-arrayen rekursivt. For et flatt dokument uten mellomliggende Pages-noder er gjennomgangen rett frem:
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;
Etter at dette er kjørt, er PageArr[0] den første siden en visningsapp ville vise, uavhengig av hvor det objektet sitter i bytestrømmen. Indekser sendt fra innringere som forutsetter visningsrekkefølge kartlegges nå riktig, og områdefeilene stopper
Harde kodede omgåelser forverrer problemet
I kodebaser der hovedårsaken aldri ble identifisert, er det vanlig å finne heuristiske lapper: bytt om første og siste side hvis totalantallet er 3, roter indeksen for dokumenter fra en spesifikk generator, legg til en forskyvning når det første objektnummeret overstiger en grense. Hver av disse lappene passer akkurat det settet med testfiler som var tilgjengelige da den ble skrevet. Legg til en annen PDF-kilde, og en av lappene utløses på feil tidspunkt, noe som produserer en indeks som nå er dobbelt feil: feil fordi den ble beregnet ut fra en array i feil rekkefølge, og feil igjen fordi en upassende kartlegging ble lagt på toppen. Områdekontrollen fanger den et sted lenger nedstrøms, og stakksporet peker ingen nyttige steder
Den eneste produktive veien er å fjerne hver heuristiske kartlegging og erstatte konstruksjonen av side-arrayen med en skikkelig tregjennomgang. Når indeksene er korrekte i utgangspunktet, trengs ingen lapper, og områdekontrollen blir en ressurs i stedet for en hindring
Hvis du vedlikeholder et bibliotek som utviser dette mønsteret, aktiver områdekontroll midlertidig i et Release-bygg og kjør det mot et mangfoldig utvalg av PDF-er: dokumenter produsert av Word, av LaTeX, av skanner-fastvare, av verktøy for deling fra PDF til PDF. Filene som utløser unntak er de der lagringsrekkefølgen for sideobjektene avviker fra gjennomgangsrekkefølgen koden din forutsetter. Hver og en er et datapunkt, ikke en egen feil
For ny kode som anroper et Delphi PDF-bibliotek, er det praktiske rådet å behandle bibliotekets sideantall som autoritativt og aldri sende en indeks avledet fra aritmetikk på eksterne data uten først å bekrefte at den faller innenfor 0..PageCount - 1. HotPDF-komponenten gjør det oppløste sideantallet tilgjengelig gjennom THotPDF.PageCount etter BeginDoc eller etter innlasting av et dokument; den verdien gjenspeiler alltid gjennomgangen av sidetreet og er trygg å bruke som øvre grense for all indeksaritmetikk