Tekninen artikkeli

HotPDF Free Pascalilla: Deflate, AES ja koodekkien rajat

HotPDF kääntyy ja toimii Free Pascal 3.2.2:lla Lazaruksen kanssa, ja tuon porttauksen rehellinen yhteenveto on kaksi virkettä. Dokumenttien luonti, lataus, tallennus, pakkaus, purku, salaus ja salauksen purku kaikki toimivat pelkästään Pascal-taustoilla, joten Lazarus-sovellus voi tuottaa ja kuluttaa aitoa PDF:ää ilman mitään C-riippuvuutta. Valinnaiset natiivit kuvakoodekit eivät, koska esikäännetyt Win64-objektit käyttävät COFF-muunnelmaa, jota kumpikaan Free Pascal -linkkeri ei voi nielaista, joten kyseisellä työkaluketjulla sisääntulopisteet ratkeavat stubeiksi, jotka epäonnistuvat suljettuina

HotPDF:n Free Pascal -kyvykkyyskartta: toimivat Pascal-deflate, AES ja dokumenttitaustat rinnalla kuvakoodekkien stubien kanssa, jotka epäonnistuvat suljettuina
Dokumentti-, pakkaus- ja salausominaisuudet toimivat pelkästään Pascal-taustoilla, kun taas natiivit kuvakoodekit ratkeavat stubeiksi, jotka epäonnistuvat suljettuina

Siirtyminen ”kääntyy” -tasolta ”toimii” -tasolle vaati tietyn joukon korjauksia, ja jokainen niistä on ansa, joka löytää minkä tahansa muun Delphi-koodikannan, joka siirtyy Free Pascalille. Ne kannattaa kirjata ylös siinä järjestyksessä, jossa ne satuttavat

Miksi yksikön kääntyminen ei todista mitään?

Koska Pascal-yksikkö voi viitata symboliin, joka ei koskaan tee mitään hyödyllistä, ja silti tyydyttää kääntäjän. Siinä vaiheessa, kun kaikki 113 kirjastoyksikköä kääntyivät puhtaasti Free Pascalilla, arkistosäiliön käsittelijät toimivat aidosti, varmennettuna savutestillä, joka avasi CBZ:n ja muunsi sen PDF:ksi. XFA-lomakkeiden litistys ei toiminut lainkaan, koska litistyksen on purettava pakattu /XFA-pakkavirta ja deflate-sisääntulopiste oli yhä stub. Mikään käännöstulosteessa ei erottanut näitä kahta tapausta

Siitä syntynyt sääntö on lyhyt. Ennen kuin kirjoitat julkaisumuistiinpanoon, että ominaisuus toimii uudella työkaluketjulla, kirjoita ajonaikainen koetin, joka kokeilee ominaisuutta päästä päähän kyseisellä työkaluketjulla. Kääntökatteisuus on edellytys, ei koskaan näyttö. Laajempi kuva siitä, mitä porttaus kattaa, on artikkelissa Free Pascal- ja Lazarus Win64 -tuen muistiinpanoissa

Heitto cdecl-stubin sisällä ei yllä kutsujaan

Tämä ansaitsee oman osionsa, koska oire on niin harhaanjohtava. Stub-yksiköt paljastavat C-sisääntulopisteet samalla tavalla kuin staattinen kirjasto, joten stub näyttää tältä

// Näyttää järkevältä. Ei ole.
function inflate(Strm: Pointer; Flush: Integer): PtrUInt; cdecl;
  public name 'inflate';
begin
  raise ENotSupportedException.Create('codec unavailable');
end;

Free Pascalilla Win64:lle tuo poikkeus ei etene kutsujalle. Ei ole try..except-käsittelijää, joka näkisi sen, koska purkautuminen tällä tavalla julistetun cdecl-rajan yli ei kanna Pascal-poikkeuskehystä; prosessi päättyy poistumiskoodilla 217. Sovelluksen näkökulmasta ei ole virhettä, ei viestiä eikä lokiriviä, vain ohjelma, joka katoaa. Se on ankarasti pahempaa kuin väärä vastaus, koska väärän vastauksen voi käsitellä

Miksi cdecl-stubin sisällä heitetty poikkeus päättää Free Pascal -prosessin poistumiskoodiin 217 ja miten Pascal-sisääntulopisteen portitus korjaa sen
Pascal-poikkeuskehys ei voi purkautua cdecl-rajan yli, joten prosessi kuolee hiljaa; korjaus portittaa ennen kuin stubiin koskaan päädytään

Kiusaava korjaus on saada stub palauttamaan vikakoodi sen sijaan, ja inflaten kohdalla se on oikein, koska zlibilla on hyvin määritelty virhepaluu. Se on väärin yleisesti: stub funktiolle jpeg_read_header, joka palauttaa nollan, käskee kutsujaa jatkamaan rakenteella, jota kukaan ei alustanut. Kestävä korjaus on portittaa Pascal-sisääntulopisteessä pikemminkin kuin C-muotoisen stubin sisällä käyttäen mitä tahansa vikakäytäntöä, jota kyseisellä API:lla jo on

function TryDecodeJPEG(const Data: TBytes; out Bitmap: TBitmap): Boolean;
begin
{$IFDEF FPC}
  // Kieltäydy ennen kuin stubiin koskaan päädytään, tällä API:lla
  // itsellään olevalla vikakäytännöllä poikkeuksen sijaan cdecl-rajan yli
  Bitmap := nil;
  Result := False;
  Exit;
{$ENDIF}
  Result := DecodeJPEGNative(Data, Bitmap);
end;

paszlib ei ole zlib, ja ero on kaksi dokumenttiluokkaa

Free Pascalilla saatavilla oleva Pascal-deflate-toteutus käsittelee kaksi kehystystä: zlib-kääreen ja raakan deflaten. Se ei käsittele gzip-kehystystä, jonka zlib valitsee windowBits-arvoilla 16:sta 31:een, eikä se käsittele automaattisen tunnistuksen tilaa, jonka arvot 32:sta 47:ään valitsevat. HotPDF tarvitsee molemmat. Turvallinen SVG-tuontipolku pyytää arvoa 31, ja lataajalla on varapolkutikapuu, joka pyytää arvoa 47, kun virran kehystys on moniselitteinen. Ohita jompikumpi, ja kokonainen dokumenttiperhe lakkaa avautumasta, dekoodausvirheellä, joka osoittaa virtaan eikä puuttuvaan kehystykseen

windowBits-kattavuus paszlibissa verrattuna zlibiin: gzip-kehystys ja automaattisen tunnistuksen alueet puuttuvat HotPDF:n SVG-tuonnista ja lataajan varapolulta
paszlib käsittelee zlib-kääreen ja raakan deflaten, mutta HotPDF tarvitsee myös windowBits 31:n ja 47:n, joten shimillä on toimitettava gzip-kehystys itse

On toinen, terävämpi yhteensopimattomuus. z_stream-tietue, jonka paszlib julistaa, ei ole samassa muistiasettelussa kuin C:n: sen msg-kenttä on lyhyt merkkijono osoittimen sijaan, ja total_in ja total_out ovat 64-bittisiä siellä, missä C-ABI:ssä on konesanonia. Kutsujan tietuetta ei siksi voi välittää suoraan läpi. Toimiva järjestely on pitää paszlibin tila state-osoittimen takana, jonka julkinen tietue jo varaa, ja kopioida julkiset kentät sisään ja ulos jokaisen kutsun ympärillä. Gzipin CRC ja kahdeksan tavun pituusperä vaativat huomion samassa shim-kerroksessa, mikä on niille luonnollinen paikka, koska se jo omistaa kehystyspäätöksen

Dynaamisen taulukon välittäminen tyypittömälle var-parametrille

Tämä on virhe, joka todennäköisimmin istuu koodissasi juuri nyt. Kun välität dynaamisen taulukon tyypittömälle var-parametrille, se, mitä vastaanottava osapuoli saa, on taulukkomuuttujan osoite, joka on osoittimen osoite, ei payloadin osoite. Lukeminen siihen ylikirjoittaa siis muuttujan itsensä ja sen, mitä sen vieressä istuu

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

  // Väärin: luovuttaa FBuffer-muuttujan osoitteen
  FStream.Read(FBuffer, Length(FBuffer));

  // Oikein: luovuttaa ensimmäisen payload-tavun osoitteen
  FStream.Read(FBuffer[0], Length(FBuffer));
end;

Delphillä väärä muoto vaikuttaa usein toimivan, koska se, mitä se korruptoi, on viereinen pinopaikka, jota mikään ei lue sen jälkeen. Free Pascalilla sama rivi aiheuttaa segmenttivirheen ensimmäisellä käytöllä. Sen tekee niin vaikeaksi havaita silmällä, että staattisilla taulukoilla ei ole tällaista ongelmaa, koska staattinen taulukkomuuttuja on oma payloadinsa, joten molemmat kirjoitusasut ovat oikein samassa tiedostossa riippuen muutaman sadan rivin päässä olevasta deklaraatiosta

ZIP-säiliöt ilman System.Zip-yksikköä

Free Pascalilla ei ole RTL:n zip-yksikön vastaista, ja saatavilla olevalla vaihtoehdolla on sekä erilainen API-pinta ettei sillä ole tukea perinnesalaukselle, jota vanhemmat säiliömuodot yhä käyttävät, joten pieni kirjaston sisäinen lukija osoittautui lyhyemmäksi kuin siihen sopeutuminen. Kaksi muotoyksityiskohtaa veivät aikaa ja ovat helppoja saada väärin

Ensimmäinen on salausotsikon tarkistustavu. Sen kahdestoista tavu on normaalisti CRC:n ylempi tavu, mutta kun yleistarkoituksisen lipun bitti 3 on asetettu, mikä tarkoittaa, että koot asuvat perässä olevassa datadeskriptorissa eikä CRC:tä vielä tiedetä, tarkistustavu tulee muutosajan ylemmästä tavusta sen sijaan. Toteuta vain CRC-muoto, ja jokainen suoratoistotilassa kirjoitettu arkisto hylkää oikean salasanan. Toinen on ZIP64-lisäkenttä: sen kolme 64-bittistä kenttää ilmestyvät kiinteässä järjestyksessä, mutta ne kirjoitetaan vain, kun vastaava 32-bittinen kenttä on kyllästynyt, joten niiden lukeminen kiinteillä siirtymillä toimii arkistoissa, jotka testasit, ja epäonnistuu seuraavassa. Jäsennä ne positionaalisesti sen mukaan, mitkä 32-bittiset kentät ovat kyllästyneet

Yksi kätevyys, joka kannattaa tuntea: Free Pascalin purkuvirta ottaa toisen konstruktoriargumentin, joka ohittaa zlib-otsikon, ja se on juuri se, mitä ZIP-merkinnät tarvitsevat, koska ne tallentavat raakan deflaten. Tuo polku ei kosketa kirjaston zlib-shimiä lainkaan, joten siihen ei vaikuta puuttuva C-tausta

Väriglyfien läpinäkyvyys LCL:n alla

Rasterisoidun väriglyfin alfakanavan lukeminen on se yksi grafiikkayksityiskohta, jolla ei ole suoraa käännöstä. LCL:n PNG-luokalla ei ole scanline-hakufunktiota, joka paljastaisi alfakanavan, ja PNG:n sijoittaminen bittikarttaan hylkää sen, joten väri-emoji saapuu täysin läpinäkymättömänä ja kompositoituu mustan laatikon kanssa takanaan. Toimiva reitti on rajapintakuva: luo se PNG:stä, lue sitten pikselit värihakufunktion kautta muistaen, että sen komponentit ovat 16-bittisiä ja ne pitää siirtää alas kahdeksalla tavuiksi. Tuo pinta käyttää myös luonnollista ylhäältä alas -rivijärjestystä, joten Height - 1 - Y-käänteisyys, jota VCL-scanline-koodi tarvitsee, on poistettava portattamisen sijaan

Kaksi käännösjärjestelmähuomiota ennen virheen raportoimista

Täysi uudelleenkäännös epäonnistuu joskus määrittelemättömään symboliin, jonka nimi päättyy $crc-päätteeseen ja heksaarvoon. Tuo pääte lasketaan parametrityypeistä, ja se ei täsmää, kun yksi käännös kääntää yksikön kahta eri rajapintaversiota vastaan samassa ajossa. Käännöksen ajaminen uudelleen poistaa sen; signatuuri ei ole väärä

Toiseksi, Free Pascal 3.2.2:lla ei ole anonyymejä metodeja, joten siellä missä kirjasto käytti sulkeumia rinnakkaisen putken kytkemiseen Free Pascal -käännös ottaa deterministisen sarjallisen varatien. Tuloste on identtinen, läpäisy ei ole; jos riipput rinnakkaisesta sivujen renderöinnistä, se on syy jäädä Delphiin toistaiseksi, ja putken suunnittelu on kuvattu artikkelissa rinnakkainen renderöintiputki ja takapaine. Kuvakoodekkien tilanne on toinen paikka, jossa työkaluketjun valinta muuttaa kyvykkyyttä eikä vain nopeutta, joten Lazarus-käyttöönoton kannattaa suunnitella kuvaformaatit sen mukaan; nykyinen työkaluketjukohtainen matriisi on HotPDF Delphi PDF -komponentin tuotesivulla