Odborný článok

HotPDF vo Free Pascale: Deflate, AES a limity kodekov

HotPDF sa kompiluje a beží pod Free Pascalom 3.2.2 s Lazarusom a úprimné zhrnutie tohto portu sú dve vety. Tvorba dokumentov, načítavanie, ukladanie, komprimácia, dekomprimácia, šifrovanie aj dešifrovanie fungujú na čisto pascaleských backendoch, takže aplikácia v Lazaruse dokáže produkovať a konzumovať skutočné PDF bez akejkoľvek C závislosti. Voliteľné natívne obrazové kodeky nie, pretože predpripravené Win64 objekty používajú variantu COFF, ktorú nedokáže skonzumovať ani jeden linker Free Pascalu, takže na tomto toolchainu sa vstupné body rozložia na stuby, ktoré zlyhávajú zatvorené

Mapa schopností HotPDF vo Free Pascale: funkčné pascaleské backendy deflate, AES a dokumentov vedľa stubov obrazových kodekov, ktoré zlyhávajú zatvorené
Funkcie dokumentov, komprimácie a šifrovania bežia na čisto pascaleských backendoch, zatiaľ čo natívne obrazové kodeky sa rozložia na stuby zlyhávajúce zatvorené

Dostať sa od „kompiluje sa“ k „funguje“ stálo špecifickú sadu opráv a každá z nich je pasca, ktorá nájde každú inú kódovú bázu Delphi smerujúcu do Free Pascalu. Stojí za to si ich zapísať v poradí, v akom boleli

Prečo kompilujúca sa jednotka nedokazuje nič?

Pretože pascaleská jednotka môže odkazovať na symbol, ktorý nikdy nespraví nič užitočné, a stále uspokojiť kompilátor. V momente, keď sa všetkých 113 jednotiek knižnice zostavilo čisto pod Free Pascalom, handlery archívnych kontajnerov naozaj fungovali, overené smoke testom, ktorý otvoril CBZ a previedol ho na PDF. Zrovnávanie XFA formulárov nefungovalo vôbec, pretože zrovnávanie musí nafúknuť komprimovaný stream paketov /XFA a vstupný bod deflate bol ešte stub. Nič vo výstupi buildu nerozoznalo tieto dva prípady

Pravidlo, ktoré z toho vzišlo, je krátke. Skôr než napíšete do poznámky k vydaniu, že funkcia funguje na novom toolchainu, napíšte runtime sondu, ktorá precvičí funkciu od začiatku do konca na tom toolchaine. Kompilačné pokrytie je predpoklad, nikdy dôkaz. Širší obraz toho, čo port pokrýva, je v poznámkach o podpore Free Pascal a Lazarus Win64

Výnimka vo vnútri cdecl stubu sa nedostane ku volajúcemu

Toto si zaslúži vlastnú sekciu, pretože príznak je tak zavádzajúci. Jednotky stubov vystavujú C vstupné body tak, ako by to robila statická knižnica, takže stub vyzerá takto

// Vyzerá rozumne. Nie je.
function inflate(Strm: Pointer; Flush: Integer): PtrUInt; cdecl;
  public name 'inflate';
begin
  raise ENotSupportedException.Create('codec unavailable');
end;

Na Free Pascale pre Win64 sa táto výnimka nešíri ku volajúcemu. Neexistuje handler try..except, ktorý by ju videl, pretože odvíjanie cez hranicu cdecl deklarovanú týmto spôsobom neprenáša pascaleský výnimkový rámec; proces sa ukončí s exit kódom 217. Zo strany aplikácie nie je žiadna chyba, žiadna správa a žiadny riadok logu, len program, ktorý zmizne. To je prísne horšie než nesprávna odpoveď, pretože nesprávna odpoveď sa dá spracovať

Prečo výnimka vyhodená vo vnútri cdecl stubu ukončí proces Free Pascalu s exit kódom 217 a ako to opraví stráženie pascaleského vstupného bodu
Pascaleský výnimkový rámec sa nedokáže odvinúť cez hranicu cdecl, takže proces zomrie potichu; oprava stráži skôr, než sa stub vôbec dosiahne

Lákavá oprava je prinútiť stub vraciať namiesto toho chybový kód a pri inflate je to správne, pretože zlib má dobre definovaný chybový návrat. Vo všeobecnosti je to zlé: stub pre jpeg_read_header, ktorý vracia nulu, hovorí volajúcemu, aby pokračoval so štruktúrou, ktorú neinicializoval nikto. Trvanlivá oprava je strážiť na pascaleskom vstupnom bode namiesto vnútra C-tvarového stubu, pričom sa použije akákoľvek konvencia zlyhania, ktorú dané API už má

function TryDecodeJPEG(const Data: TBytes; out Bitmap: TBitmap): Boolean;
begin
{$IFDEF FPC}
  // Odmietnuť skôr, než sa stub vôbec dosiahne, s vlastnou konvenciou
  // zlyhania tohto API namiesto výnimky cez cdecl
  Bitmap := nil;
  Result := False;
  Exit;
{$ENDIF}
  Result := DecodeJPEGNative(Data, Bitmap);
end;

paszlib nie je zlib a rozdiel sú dve triedy dokumentov

Pascaleská implementácia deflate dostupná vo Free Pascale spracováva dva rámce: obal zlib a surový deflate. Nespracováva rámec gzip, ktorý zlib vyberá cez hodnoty windowBits od 16 do 31, a nespracováva režim automatickej detekcie, ktorý vyberajú hodnoty 32 až 47. HotPDF potrebuje oboje. Bezpečná cesta importu SVG žiada 31 a loader má rebrík fallbackov, ktorý žiada 47, keď je rámec streamu nejasný. Vynecháte ktorýkoľvek z nich a celá rodina dokumentov prestane sa otvárať s dekódovacou chybou ukazujúcou na stream namiesto na chýbajúci rámec

Pokrytie windowBits paszlib oproti zlib: rámce gzip a rozsahy auto-detekcie chýbajúce pre import SVG HotPDF a fallback loadera
paszlib spracováva obal zlib a surový deflate, no HotPDF potrebuje tiež windowBits 31 a 47, takže shim musí dodať rámec gzip sám

Je tu druhá, ostrejšia nekompatibilita. Záznam z_stream, ktorý paszlib deklaruje, nemá rovnaké rozloženie pamäti ako C záznam: jeho pole msg je krátky reťazec namiesto ukazovateľa a total_in a total_out sú 64-bitové tam, kde C ABI má strojové slová. Volajúci záznam preto nemôže byť odovzdaný rovno. Funkčné usporiadanie je uchovať stav paszlib za ukazovateľom state, ktorý verejný záznam už rezervuje, a kopírovať verejné polia dnu a von okolo každého volania. CRC gzipu a osembajtová prípojka dĺžky sa riešia v tej istej vrstve shimu, čo je pre ne prirodzené miesto, keďže tá už vlastní rozhodnutie o rámci

Odovzdanie dynamického poľa netypovanému var parametru

Toto je chyba, ktorá je najpravdepodobnejšie práve teraz vo vašom kóde. Keď odovzdáte dynamické pole netypovanému parametru var, to, čo volaný prijíma, je adresa premennej poľa, čo je adresa ukazovateľa, nie adresa užitočného zaťaženia. Čítanie do neho teda prepíše samotnú premennú a všetko, čo sedí vedľa nej

var
  FBuffer: TBytes;
begin
  SetLength(FBuffer, 65536);

  // Zle: odovzdá adresu premennej FBuffer
  FStream.Read(FBuffer, Length(FBuffer));

  // Správne: odovzdá adresu prvého bajtu dát
  FStream.Read(FBuffer[0], Length(FBuffer));
end;

V Delphi sa nesprávna forma často zdá fungovať, pretože to, čo poškodí, je susedný slot zásobníka, ktorý potom nič nečíta. Vo Free Pascale ten istý riadok spôsobí segmentation fault pri prvom použití. To, čo to robí tak ťažko spoznateľným na prvý pohľad, je, že statické polia nemajú takýto problém, keďže premenná statického poľa je vlastným užitočným zaťažením, takže oba zápisy sú správne v tom istom súbore v závislosti od deklarácie pár stoviek riadkov ďalej

Kontajnery ZIP bez System.Zip

Free Pascal nemá ekvivalent zip jednotky RTL a dostupná alternatíva má odlišný API povrch aj absenciu podpory legacy šifrovania, ktoré staršie formáty kontajnerov stále používajú, takže malý čítač priamo v knižnici vyšiel kratšie než prispôsobovanie sa jej. Dva detaily formátu stáli čas a dajú sa ľahko pokaziť

Prvý je kontrolný bajt hlavičky šifrovania. Jeho dvanásty bajt je normálne vysoký bajt CRC, ale keď je nastavený bit 3 všeobecného príznaku, čo znamená, že veľkosti bývajú v koncovom data descriptore a CRC ešte nie je známe, kontrolný bajt pochádza namiesto toho z vysokého bajtu času úpravy. Ak implementujete len formu CRC, každý archív zapísaný v streamovacom režime odmietne správne heslo. Druhý je extra pole ZIP64: jeho tri 64-bitové polia sa objavujú v pevnom poradí, ale zapisujú sa len vtedy, keď je zodpovedajúce 32-bitové pole nasýtené, takže čítanie ich na pevných offsetoch funguje na archívoch, ktoré ste testovali, a zlyhá na ďalšom. Parseujte ich pozične podľa toho, ktoré 32-bitové polia sú nasýtené

Jedno pohodlie, ktoré stojí za znalosť: dekompresný stream Free Pascalu berie druhý argument konštruktora, ktorý preskočí hlavičku zlib, čo je presne to, čo potrebujú položky ZIP, keďže uchovávajú surový deflate. Táto cesta sa knižničného zlib shimu vôbec nedotýka, takže nie je ovplyvnená chýbajúcim C backendom

Transparentnosť farebných glyfov pod LCL

Čítanie alpha kanála rasterizovaného farebného glyfu je ten jeden grafický detail bez priameho prekladu. Trieda PNG v LCL nemá prístup k scanline, ktorý vystavuje alpha, a priradenie PNG do bitmapy ju zahodí, takže farebné emoji prichádza úplne nepriehľadné a kompozituje sa s čiernym obdĺžnikom za sebou. Funkčná cesta je interface obraz: vytvorte ho z PNG, potom čítajte pixely cez farebný prístupník s tým, že jeho komponenty sú 16-bitové a potrebujú posun dolu o osem, aby sa stali bajtmi. Tento povrch tiež používa prirodzené poradie riadkov zhora nadol, takže inverzia Height - 1 - Y, ktorú potrebuje kód VCL scanline, sa musí odobrať, nie portovať

Dve poznámky k build systému, skôr než nahlásite chybu

Úplný rebuild občas zlyhá s nedefinovaným symbolom, ktorého meno končí príponou $crc a hex hodnotou. Tá prípona sa počíta z typov parametrov a prestane sedieť, keď jeden build skompiluje jednotku proti dvom rozdielnym verziám interface v tom istom priebehu. Znovu spustený build to vyčistí; signatúra nie je zlá

Zadruhé, Free Pascal 3.2.2 nemá anonymné metódy, takže všade, kde knižnica používala closure na zapojenie paralelnej pipeline, berie build Free Pascalu namiesto toho deterministický sériový fallback. Výstup je identický, priepustnosť nie; ak závisíte na paralelnom renderovaní stránok, je to dôvod zostať zatiaľ pri Delphi a návrh pipeline popisuje článok o paralelnej renderovacej pipeline. Situácia obrazových kodekov je druhé miesto, kde voľba toolchainu mení schopnosti, nielen rýchlosť, takže nasadenie Lazarus by malo naplánovať svoje obrazové formáty podľa toho; aktuálna matica podľa toolchainov je na produktovej stránke HotPDF Delphi PDF component