Articol tehnic

Diagnosticarea eșecurilor tăcute de stub în biblioteci PDF

Când o bibliotecă Delphi câștigă o configurație de build fără framework-ul vizual, clasele-substitut sunt locul unde trăiesc erorile. Nu platforma, nu compilatorul: înlocuitorii. PDFlibPas are un strat grafic care oferă echivalente bitmap, canvas, font, metafișier și imprimantă pentru build-urile fără VCL, iar portarea lui pe Free Pascal a scos la suprafață fiecare mod de eșec pe care îl poate avea un înlocuitor. Se sortează îngrijit după cost de diagnostic, iar ordinea este opusul a ceea ce sugerează intuiția

Un înlocuitor care aruncă excepție este ieftin de găsit; excepția numește metoda. Un înlocuitor care returnează date goale este scump, deoarece eșecul apare la mai multe straturi distanță de cauza lui. Un înlocuitor care returnează succes este cel mai rău dintre toate, deoarece codul de retur este valid, codul de eroare este zero, nu se aruncă nicio excepție, iar singura dovadă că ceva a mers prost se află în octeții care au ieșit

Trei forme de eșec tăcut de stub într-o bibliotecă PDF Pascal, ierarhizate după cost de diagnostic, de la un stub care aruncă excepție la succes peste rezultat gol
Un înlocuitor care aruncă excepție este ieftin de diagnosticat, datele goale sunt scumpe, iar un retur de succes peste un artefact gol este cel mai greu de găsit

Forma trei: un identificator de imagine valid peste un XObject gol

Convertorul vectorial de metafișiere era un corp de procedură gol în configurația non-VCL. Tot ce era deasupra lui a continuat să funcționeze. Punctele de intrare de import EMF și punctul de intrare de captură canvas au rulat până la capăt și au returnat un identificator de imagine legal, pe care apelantul l-a plasat apoi pe o pagină. Ce a ajuns în fișier a fost un form XObject cu lungime de conținut zero. Pagina s-a randat albă

Nimic nu a raportat o problemă, și asta include programul demonstrativ propriu al bibliotecii pentru această funcție, care a desenat o pagină goală și nu a observat. Nu exista nicio valoare de retur eșuată de verificat, deoarece secvența de apeluri chiar a reușit în întregime; singurul lucru greșit era mărimea stream-ului produs. Diagnosticarea acestei clase de defecte înseamnă a pune o întrebare diferită: nu „a eșuat apelul", ci „este artefactul plauzibil". Un form XObject de lungime zero, o imagine de zero pixeli, o pagină de zero octeți de conținut: acestea sunt aserțiunile care îl prind

Remedierea are două jumătăți, iar a doua este ușor de uitat. În primul rând, faceți implementarea goală să arunce excepție, astfel încât eșecul să aibă măcar un canal. În al doilea rând, convertiți acea excepție într-un rezultat null la fabrica de imagini și adăugați verificări null la cele două locuri care consumă un identificator de imagine, deoarece altfel „eșecul curat" se transformă direct într-o încălcare de acces pe măsură ce arborele de pagini dereferențiază nimic. Un stub care aruncă este o îmbunătățire doar dacă apelanții erau pregătiți pentru un eșec pe care nu fuseseră niciodată capabili să îl primească înainte

Forma doi: date goale, la trei straturi de crash

Înlocuitorul de canvas pentru metafișier nu își completa dimensiunile fizice. Valoarea aceea se împarte într-un calcul de geometrie a paginii, deci calculul producea zero, deci calculul bounding-box împărțea la zero. Un handler de excepție gol a înghițit asta, fabrica de imagini a returnat un rezultat null, iar încălcarea de acces s-a întâmplat în final în arborele de pagini când null-ul a fost folosit. Trei straturi între cauză și simptom, cu un handler de excepție în mijloc ștergând dovezile

Lanțul de eșec al unui înlocuitor gol de canvas pentru metafișier în PDFlibPas, ajungând la o încălcare de acces la trei straturi după împărțirea la zero
Dimensiunea goală reduce un calcul de geometrie la zero, un handler gol șterge excepția, iar identificatorul null face să crape arborele de pagini

Aceeași unitate avea încă două instanțe ale tiparului. Clasa de fonturi avea corpuri Assign și de constructor goale, ceea ce contează mai mult decât pare, deoarece proprietatea de font a canvasului este read-only: atribuirea în ea este singura cale de a livra un font, deci o implementare goală face selecția de fonturi inefectivă în tăcere, iar textul iese în orice era implicit. Iar o valoare de pixeli pe inch de zero făcea ca fiecare apelant care dimensiona un canvas pe baza metricilor de font să producă un canvas de zero pe zero, ceea ce dă o pagină goală și un retur de succes

// Tiparul de căutat într-o unitate-substitut: o metodă care nici
// nu aruncă excepție, nici nu face nimic. Ambele compilează și
// ambele produc "succes" fără niciun rezultat
procedure TMetafileCanvasStandIn.Create(...);
begin
  // niciun apel inherited, nicio inițializare de câmp
end;

function TBitmapStandIn.LoadFromStream(Stream: TStream): Boolean;
begin
  Result := True;    // iar bitmapul este încă gol
end;

Structura wide care păstrează doar primul caracter

Aceasta nu este deloc o problemă de înlocuitor, dar aparține aceluiași catalog deoarece simptomul este la fel de departe de cauză. Structura de enumerare a imprimantelor a fost declarată cu toți cei doisprezece membri de tip string dați ca pointeri către caractere de un octet, în timp ce funcția care o umple este varianta cu caractere wide a API-ului de enumerare

Mărimile pointerilor sunt identice, deci aspectul structurii este corect și nimic nu crapă. Ce se întâmplă în schimb este că citirea unui string UTF-16 ca string de un octet se oprește la primul octet zero, care pentru orice nume de imprimantă ASCII este jumătatea superioară a celui de-al doilea caracter. Fiecare nume de imprimantă a venit exact ca un singur caracter. În aval, validarea numelui a eșuat, crearea imprimantei a eșuat și tipărirea a eșuat pentru fiecare imprimantă reală de pe mașină, iar niciunul dintre aceste simptome nu indică o declarație de structură

Nume de imprimantă UTF-16 trunchiat la un caracter după ce o structură Win32 wide a fost declarată cu membri PAnsiChar în loc de PWideChar
Membrii de un octet citesc un nume UTF-16 doar până la primul său octet zero, deci fiecare nume de imprimantă revine cu exact un caracter
// Greșit: mărimea corectă, tip de element greșit. Fără eroare de
// compilare, fără crash, fiecare string trunchiat la un caracter
type
  TPrinterInfo2Wrong = record
    pServerName: PAnsiChar;
    pPrinterName: PAnsiChar;
    // ... încă zece
  end;

// Corect: o structură *W are membri wide peste tot
type
  TPrinterInfo2W = record
    pServerName: PWideChar;
    pPrinterName: PWideChar;
    // ... încă zece
  end;

Regula care rezultă este mecanică și merită aplicată fără gândire: pentru orice structură Win32 al cărui nume se termină în W, verificați că fiecare membru string este varianta wide, câmp cu câmp. Amestecarea lumilor ANSI și wide nu produce niciun diagnostic de compilator și niciun crash, doar trunchiere tăcută, iar același lucru se aplică invers pentru variantele ANSI

Un handler de excepție gol este adevăratul adversar

Fiecare dintre aceste investigații a fost încetinită de același construct: un handler care prinde tot și îl convertește într-o valoare de retur falsă. Este un lucru rezonabil de scris în jurul unui decoder de imagini, deoarece o imagine coruptă nu ar trebui să doboare o sarcină de document. Este, de asemenea, un dispozitiv pentru ștergerea singurei informații de care aveți nevoie

Răspunsul practic este să faceți handlerul temporar zgomotos. Descărcarea clasei de excepție, a mesajului și a backtrace din interiorul handlerului gol, sub o condițională de debug, convertește un retur null neexplicat într-o excepție numită cu o locație. În două dintre cele trei cazuri de mai sus acel singur pas a încheiat investigația, deoarece excepția era o împărțire la zero sau o încălcare de acces într-o metodă de înlocuitor al cărei nume spunea totul

Lista de verificare pentru adoptarea unei căi de înlocuitor

Patru puncte, în ordinea în care dau roade. Înainte de a apela o clasă-substitut, citiți metodele pe care urmează să le folosiți și confirmați că fiecare are un corp real; un corp gol nu este un detaliu de implementare, este o funcție lipsă. Preferați înlocuitorii care aruncă excepție celor care returnează valori neutre, și împerecheați asta cu verificări null la locurile în care o fabrică poate acum legitim să nu returneze nimic. Verificați o funcție inspectând artefactul, nu codul de retur, deoarece întregul mod de eșec de aici este un cod de retur curat peste un artefact gol; o defalcare la nivel de octet a ceea ce conține efectiv un document este calea cea mai rapidă de a o vedea, iar articolul despre auditul mărimii fișierelor acoperă acel tooling. Iar când o funcție nu are nicio implementare-substitut viabilă, îndreptați mostrele afectate spre calea care funcționează și spuneți de ce într-un comentariu, în loc să lăsați o demonstrație care produce în tăcere rezultate goale

Punctul mai larg se aplică dincolo de o singură bibliotecă. Orice bază de cod cu o a doua implementare condițională, un strat mock, un mod headless, un shim de platformă, este expusă formei trei. Motivul pentru care se ascunde atât de bine este că fiecare poartă de calitate pe care se bazează în mod normal o echipă — coduri de retur, coduri de eroare, excepții, stări de ieșire — este un canal de stare, iar forma trei le ține pe toate curate. Doar rezultatul o trădează. Acesta este și raționamentul din spatele verificării artefactelor în locul stărilor la manipularea datelor de intrare nesigure, descris în articolul despre parsarea PDF nesigură, și din spatele comparării rezultatelor randate între motoare în loc să aveți încredere într-unul singur, descris în randarea multi-motor

PDFlibPas este o bibliotecă PDF nativă în Object Pascal pentru Delphi, C++Builder și Free Pascal, iar configurația ei non-VCL este ceea ce face posibile build-urile headless și cross-toolchain; acoperirea actuală de configurații este listată pe pagina de produs losLab PDF Developer Library