Articol tehnic

Erori de verificare a intervalului în biblioteci PDF Delphi

Erorile de verificare a intervalului din bibliotecile PDF pentru Delphi și-au câștigat reputația de a fi greu de localizat, pentru că nu urmează un tipar de intrare consecvent. Același document le produce pe o mașină și nu pe alta; aceeași cale de cod declanșează excepția pe un fișier de 3 pagini, dar rulează curat pe unul de 12. Acea inconsecvență se reduce aproape întotdeauna la o singură cauză de fond: obiectele de pagină PDF nu sunt stocate în ordinea din fișier. Dacă biblioteca își construiește tabloul intern de pagini scanând obiectele secvențial, în loc să parcurgă arborele de pagini declarat de catalog, ea construiește un index al cărui interval valid nu corespunde cu ceea ce așteaptă apelanții, iar verificarea intervalului prinde această nepotrivire în cel mai prost moment posibil

Cum funcționează verificarea intervalului în Delphi

Cu directiva de compilare {$R+} activă (valoarea implicită în configurația Debug), RTL-ul Delphi validează la execuție fiecare index de tablou, fiecare indice de șir și fiecare atribuire de tip enumerat. Un acces în afara limitelor ridică ERangeError, în loc să citească în tăcere memoria alăturată. Acel comportament este valoros: scoate la suprafață devreme defectele latente, în loc să le lase să corupă o structură de date care abia peste o sută de linii cedează. Partea frustrantă este că excepția se declanșează la locul accesului, nu în punctul în care indexul a fost calculat greșit. Când stiva de apeluri arată o metodă adânc imbricată dintr-o unitate PDF, greșeala reală se află de obicei cu mai multe cadre în urmă

Condițiile booleene compuse înrăutățesc situația. Delphi evaluează expresiile and de la stânga la dreapta, cu semantică de scurtcircuit, dar scurtcircuitul sare peste evaluare doar când partea stângă este False. O expresie ca:

if FDocStarted and (DestIndex < Length(PageArr)) and
   (PageArr[DestIndex].PageObj <> nil) then

pare sigură, dar protejează împotriva unui index în afara intervalului numai dacă FDocStarted este True și DestIndex nu este negativ. Verificarea DestIndex < Length(PageArr) nu face nimic atunci când DestIndex este negativ, pentru că, în aritmetica cu semn, compararea unui întreg negativ cu o lungime nenegativă returnează True, iar accesul la tablou care urmează tot declanșează eroarea de interval. Mutarea verificării limitelor pe poziția cea mai exterioară este corectura potrivită:

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]);

Aceasta este corectura mecanică. Ea oprește căderea. Nu explică de ce DestIndex a primit de la bun început o valoare în afara intervalului valid

Diagramă de flux a unei erori Delphi de verificare a intervalului într-o bibliotecă PDF, în care un DestIndex negativ ocolește un control de limite pe o singură parte și ridică ERangeError la accesul în tablou, comparată cu ordinea corectată a gărzilor, care scurtcircuitează în siguranță
Sub $R+ RTL-ul validează fiecare indice la execuție, așa că un index negativ trece de un simplu test al limitei superioare și se declanșează chiar la accesul în tablou. Testarea mai întâi a limitei inferioare cu semn lasă evaluarea prin scurtcircuit să îl respingă înainte de atingerea vreunui element

Cauza reală: ordinea obiectelor față de ordinea paginilor

ISO 32000-1 §7.7.3 definește arborele de pagini ca pe un arbore de noduri Pages ale căror tablouri Kids listează obiectele de pagină în ordinea de afișare. Fișierul stochează acele obiecte la orice decalaje a nimerit să aleagă generatorul; obiectul cu numărul 20 poate precede fizic obiectul cu numărul 3 în fluxul de octeți. O bibliotecă ce își construiește lista de pagini iterând tabelul de referințe încrucișate în ordinea numerelor de obiect, în loc să urmeze lanțul Kids, va produce o secvență care se abate de la ceea ce așteaptă utilizatorul. Pe documentele în care generatorul a nimerit să scrie paginile în ordine, totul funcționează. Pe documentele în care nu a făcut-o, discrepanța dintre numerotarea paginilor din bibliotecă și numerotarea paginilor la apelant produce indici care cad în afara lui PageArr

Diagramă Delphi care pune în contrast ordinea de stocare a obiectelor PDF descoperită printr-o scanare a referințelor încrucișate cu ordinea de afișare recuperată prin parcurgerea tabloului Kids al arborelui de pagini pornind de la catalog
Scanările secvențiale numerotează paginile după ordinea aleasă de generator, în timp ce vizualizatoarele urmează tabloul Kids al arborelui de pagini. Construirea tabloului de pagini din parcurgerea catalogului menține indicii apelantului aliniați și în interval

Abordarea corectă este să porniți de la catalog, să rezolvați referința indirectă /Pages și să parcurgeți recursiv tabloul Kids. Pentru un document plat, fără noduri Pages intermediare, parcurgerea este directă:

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
      // nod intermediar: recursivitate în tabloul lui Kids
      BuildPageIndexFromTree(Child.GetArray('/Kids'), PageArr);
    end;
  end;
end;

După ce rulează aceasta, PageArr[0] este prima pagină pe care ar afișa-o un vizualizator, indiferent unde stă acel obiect în fluxul de octeți. Indicii transmiși de apelanți care presupun ordinea de afișare se mapează acum corect, iar erorile de interval încetează

Soluțiile improvizate scrise direct în cod agravează problema

În bazele de cod în care cauza de fond nu a fost niciodată identificată, se găsesc frecvent petice euristice: schimbă între ele prima și ultima pagină dacă numărul total este 3, rotește indexul pentru documentele venite de la un anumit generator, aplică un decalaj când primul număr de obiect depășește un prag. Fiecare dintre aceste petice se potrivește exact setului de fișiere de test aflate la îndemână în momentul scrierii lui. Adăugați o altă sursă de PDF-uri și unul dintre petice se declanșează la momentul nepotrivit, producând un index acum de două ori greșit: greșit pentru că a fost calculat dintr-un tablou dezordonat și greșit din nou pentru că i s-a aplicat deasupra o mapare inaplicabilă. Verificatorul de interval îl prinde undeva mai jos, iar urma de stivă nu arată spre nimic util

Singura cale productivă este să eliminați fiecare mapare euristică și să înlocuiți construirea tabloului de pagini cu o parcurgere corectă a arborelui. Odată ce indicii sunt corecți prin construcție, nu mai este nevoie de niciun petic, iar verificatorul de interval devine un atu, nu un obstacol

Dacă întrețineți o bibliotecă ce prezintă acest tipar, activați temporar verificarea intervalului într-o compilare Release și rulați-o pe un corpus divers de PDF-uri: documente produse de Word, de LaTeX, de firmware-ul unui scaner, de utilitare de divizare PDF-la-PDF. Fișierele care declanșează excepții sunt cele a căror ordine a obiectelor de pagină se abate de la ordinea de parcurgere presupusă de codul dumneavoastră. Fiecare dintre ele este un punct de date, nu un defect separat

Pentru codul nou care apelează o bibliotecă PDF pentru Delphi, sfatul practic este să tratați numărul de pagini raportat de bibliotecă drept autoritate și să nu transmiteți niciodată un index derivat din calcule pe date externe fără a confirma mai întâi că se încadrează în 0..PageCount - 1. Componenta HotPDF pentru Delphi expune numărul de pagini rezolvat prin THotPDF.PageCount după BeginDoc sau după încărcarea unui document; acea valoare reflectă întotdeauna parcurgerea arborelui de pagini și poate fi folosită în siguranță ca limită superioară pentru orice calcul de indici