Tekninen artikkeli

Vahingossa toimiva Delphi-koodi: viisi FPC-porttausbugia

PDF Library for Delphi löysi viisi dekooderivikaa siirtäessään CCITT-, TIFF-, PNG-, Flate- ja streamipuskurikoodinsa Free Pascalille, ja jokainen niistä oli läpäissyt koko Delphin testisarjan vuosien ajan. Yksikään ei ollut kääntäjävirhe. Jokainen oli Pascalia, jonka Delphi sattui suorittamaan oikein toteutuksen yksityiskohdan takia: piilossa oleva tulosparametri, joka aliaksoi kutsujan taulukon; väärän alueen haara, jonka ohi kukaan ei koskaan lukenut; nollapituinen puskuri, jonka ainoa vartija oli alueentarkistusvalitsin; 1-pohjainen siirtymä, jonka vain yksi koodipolku koskaan antoi arvolla 1; ja TStream.Read-sopimus, jota muistissa olevat streamit eivät koskaan rasita. Vaihda kääntäjä tai syötä sama koodi vialliselle tiedostolle, niin vahinko lakkaa pitämästä

Seuraavassa käydään läpi kunkin vian tarkka muoto, korjaus ja se kurinalaisuus, joka niistä syntyi: saman lähdekoodin on nykyään tuotettava samat asiakirjasemantiikat molemmilla kääntäjillä, ja testitiedosto tarkistaa, että näin tapahtuu. Sisarartikkeli Pascal-PDF-jäsentimen koventamisesta haitallisia tiedostoja vastaan käsitteli kokonaislukujen leveyttä, rekursion syvyyttä ja alustamattomia puskureita. Tämä artikkeli käsittelee eri vikaluokkaa: koodia, joka oli ollut väärin koko ajan ja jota kääntäjä hiljaa paikkaili

Miksi dynaamisen taulukon palauttava funktio toimii Delphissä ilman SetLength-kutsua?

Delphi antaa nimittäin kutsujan oman muuttujan piilotettuna tulosparametrina, joten funktio, joka ei koskaan varaa tulokselleen tilaa, voi silti kirjoittaa kutsujan varaamaan taulukkoon. TPLCCITTDecoder.GetNextChangingElement(a0: Integer; IsWhite: Boolean): TCCITTIntegerArray on viiteviivan haku, joka on kaksiulotteisen Group 3- ja Group 4 -dekoodauksen ytimessä: kun sille annetaan nykyinen sijainti a0 ja nykyisen juoksun väri, se etsii edellisen juovan muutoskohdat, ITU-T T.4- ja T.6-standardien kaksiulotteisen koodausmenetelmän b1:n ja b2:n, ja palauttaa ne kaksipaikkaisena taulukkona. Alkuperäinen funktio kirjoitti Result[0] ja Result[1] eikä kutsunut SetLength-funktiota Result-muuttujalle lainkaan

Tuon pitäisi kaatua heti ensimmäisestä kirjoituksesta, ja Free Pascalilla se kaatuukin. Delphissä se ei koskaan kaatunut, koska dekooderin molemmat kutsukohdat näyttävät tältä: julistetaan b: TCCITTIntegerArray, ajetaan SetLength(b, 2) kerran ennen juovaluuppia ja sijoitetaan sitten luupin sisällä b := GetNextChangingElement(a0, IsWhite) sekä luetaan b[0] ja b[1]. Delphin kieliopas toteaa, että funktio, jonka tulos on pitkä merkkijono, dynaaminen taulukko tai muu hallittu tyyppi, vastaanottaa tuloksensa ylimääräisenä var-parametrina, ja käytännössä kääntäjä välittää sijoituksen kohdeosoitteen. Niinpä funktion sisällä Result on b itse, jo kaksialkioinen, ja jokainen kirjoitus päätyy muistiin, jonka kutsuja omistaa. Free Pascal antaa funktiolle tuoreen nil-taulukon ja sijoittaa sen b-muuttujaan jälkikäteen, mikä on se sopimuksen tulkinta, jota vasten koodi olisi alun perinkin pitänyt kirjoittaa

PDFlibPas-CCITT-dekoodauksen ero: Delphi välittää kutsujan taulukon b GetNextChangingElement-funktion piilotettuna var Result -parametrina, jolloin kirjoitukset päätyvät kutsujan omistamaan muistiin ja osumatta jäänyt haku säilyttää edelliset arvot, kun taas Free Pascal antaa funktiolle tuoreen nil-taulukon, jonka Length-vartijan on mitoitettava SetLength-kutsulla ennen ensimmäistä kirjoitusta
Delphi aliaksoi kutsujan taulukon piilotetuksi Result-parametriksi, joten vartioimattomatkin kirjoitukset päätyvät omistettuun muistiin, kun taas Free Pascal saapuu paikalle nil-taulukolla ja yhden rivin vartija muuttaa kaatumisen tarkoitetuksi toiminnaksi Delphin dekoodauspolkua muuttamatta

Aliaksella oli mukanaan myös semantiikka, josta dekooderi on riippuvainen. Result[0] sijoitetaan vain, kun läpikäynti löytää a0-arvoa suuremman alkion, ja Result[1] vain kun sen jälkeen on vielä yksi alkio, joten osumatta jäädessä paikat säilyttävät sen, mitä edellinen iteraatio jätti b-muuttujaan. Ilmeinen korjaus, eli kahden paikan varaaminen ja niiden nollaaminen joka kutsulla, olisi tuhonnut tuon jatkumon ja muuttanut Delphin tuottamaa dekoodaustulosta. Käyttöön otettu korjaus on vartija nollauksen sijaan: Delphissä se on kuollutta koodia ja dekoodauspolku pysyy tavu tavulta sellaisena kuin oli, ja Free Pascalilla se muuttaa kaatumisen tarkoitetuksi toiminnaksi. Koko jutun ydin on juuri tuo epäsymmetria, sillä korjauksen oli oltava no-op kääntäjällä, jolla koodi tuotti jo varmennettua tulosta

Function TPLCCITTDecoder.GetNextChangingElement(a0: Integer;
  IsWhite: Boolean): TCCITTIntegerArray;
Begin
  // Delphi saapuu tänne kutsujan kaksialkioisen taulukon ollessa
  // aliaksena Result-muuttujana, joten tämä on no-op siellä. FPC saapuu nil-arvolla.
  If (Length(Result) < 2) Then
    SetLength(Result, 2);
  ...
  // Result[0] / Result[1] kirjoitetaan edelleen vain osumalla, joten
  // osumatta jäädessä arvot säilyvät täsmälleen kuten ennen
End;

Luku, joka eli pidempään kuin datansa: TIFF-hakemistomerkintä

Kun mitätöit taulukon, sinun on mitätöitävä sen lukumäärä samassa lauseessa, tai muuten lukumäärää uskoo koodi, joka ei koskaan näe itse taulukkoa. TIFF-kuvatiedoston hakemistomerkintä (TIFF 6.0 §2, tagin, tyypin, lukumäärän ja arvon-tai-siirtymän 12 tavun asettelu) kuljettaa mukanaan 32-bittisen lukumäärän suoraan tiedostosta, ja PDF Library for Delphi lukee jokaisen niistä PopDE: TTIFFEntry -funktiolla, joka palauttaa tietueen sisältäen Tag-, TagType-, Length- ja Offset-kentät sekä dekoodatut IntegerValues- ja DoubleValues-taulukot. Alkuperäinen koodi tarkisti, ylittikö Offset + TypeSize * Length tiedoston lopun, ja jos ylitti, se asetti molemmat taulukot nollapituisiksi. Se jätti Result.Length-arvon tiedostosta luettuun arvoon

Siitä seurasi kaksi ongelmaa. Funktio päättyy varamenettelyyn, joka sanoo ”jos Length on nolla, anna merkinnälle yksi nolla-arvoinen alkio”, jotta kutsujat voivat aina lukea nollannen alkion. Koska Length-arvoa ei koskaan nollattu alueen ylityksen polulla, tuo varamenettely ei koskaan lauennut siinä ainoassa tapauksessa, jota varten se oli olemassa. Ja kutsujat todella lukevat nollannen alkion ehdoitta: Width, Height, BitsPerSample, PhotometricInterpretation, FillOrder, SamplesPerPixel, RowsPerStrip ja tusina muuta ottavat E.IntegerValues[0]-arvon, ja strip-taulukot tekevät Move(E.IntegerValues[0], StripOffsets[0], E.Length * 4) kopioiden Length kertaa neljä tavua taulukosta, jossa ei ole yhtään tavua. Nollattu taulukko, jonka lukumäärä on yhä voimassa, on ehdottomasti vaarallisempi kuin tarkistamaton, sillä tarkistamaton sentään sisältää ne tavut, joita se väittää sisältävänsä

Toinen ongelma oli järjestys. Molemmat SetLength-kutsut ajettiin ennen alueen tarkistusta tiedoston lukumäärän mukaan mitoitettuina, joten vihamielinen merkintä saattoi pyytää useiden gigatavujen varausta ennen ensimmäistäkään kelpoisuustarkistusta. Delphissä syntyneen poikkeuksen nappasi kuvien latauspolun ylempänä oleva käsittelijä ja tiedosto yksinkertaisesti epäonnistui latautumaan, minkä takia kukaan ei huomannut mitään; todellisuudessa kyseessä oli muistin loppumisen tapahtuma, jonka tiedosto itse valitsi. Korjaus siirtää varauksen tarkistuksen jälkeen ja saa lukumäärän kulkemaan datan mukana

TIFF-hakemistomerkinnän koventaminen PDFlibPasissa: 12 tavun merkintä kuljettaa tiedoston antamaa lukumäärää, rikkinäinen järjestys varasi taulukot tuon lukumäärän mukaan ennen aluetarkistusta ja jätti Result.Length-arvon voimaan taulukoiden nollauksen jälkeen, ja korjattu järjestys testaa ensin Int64-aritmetiikalla tiedoston pituutta vasten, joten lukumäärä nollataan yhdessä taulukoiden kanssa
Ennen aluetarkistusta tehty varaus antoi vihamielisen lukumäärän pyytää gigatavuja ja jätti voimassa olevan lukumäärän tyhjennettyyn taulukkoon, joten korjaus testaa ensin siirtymän ja nollaa Result.Length-arvon samassa lauseessa taulukoiden kanssa
OutOfRange := Int64(ValueOffset) + Int64(TypeSize) * Result.Length
              > Length(Source);
If OutOfRange Then
Begin
  Result.Length := 0;              // lukumäärä lähtee arvojen mukana
  SetLength(Result.IntegerValues, 0);
  SetLength(Result.DoubleValues, 0);
End
Else
Begin
  SetLength(Result.IntegerValues, Result.Length);  // vasta nyt
  SetLength(Result.DoubleValues, Result.Length);
End;
// ... myöhemmin olemassa oleva varamenettely saavuttaa vihdoin tapauksensa:
If (Result.Length = 0) Then
Begin
  SetLength(Result.IntegerValues, 1);
  Result.IntegerValues[0] := 0;
End;

Tässä korjauksessa ei ole mitään kääntäjäkohtaista, ja juuri siksi se kuuluu tähän listaan. Vika oli piilevänä Delphissä samasta syystä kuin se oli piilevänä Free Pascalilla: yhdessäkään testitiedostossa ei ollut hakemistomerkintää, joka osoittaisi tiedoston loppuosan ohi. Siirto ei tuonut sitä esiin. Sen teki koodin lukeminen kysymyksen ”mitä Delphi tekee täällä puolestani, mitä en tee itse” kanssa

Mitä tapahtuu, kun PNG:n IHDR väittää värityypiksi jotain, jota formaatti ei määrittele?

PDF Library for Delphi hylkää kuvan nykyään ennen kuin juovasuodattimet ajetaan; ennen versiota v3.539.2 se laski nollatavuisen juovan ja antoi suodatinta purkaville luupeille tyhjän puskurin. ISO 15948 §11.2.2 määrittelee IHDR-lohkon, ja taulukko 11.1 luettelee kuusi laillista värityypin ja bittisyvyyden yhdistelmää: harmaasävy 1, 2, 4, 8 tai 16 bitillä, indeksoitu väri 1, 2, 4 tai 8 bitillä sekä truecolor, harmaasävy alfakanavalla ja truecolor alfakanavalla 8 tai 16 bitillä. TPNGReader validoi IHDR:n pakkausmenetelmä- ja suodatinmenetelmäkentät ja päästi FColorType-arvon ja bittisyvyyden läpi koskemattomina

Juovasuodattimen koodi mitoittaa kaiken Case FColorType Of -rakenteesta, joka kuvaa kunkin värityypin komponenttimääräksi. Kuuden ulkopuolelle jäävä värityyppi putoaa Else-haaraan, jossa SourceComponents on 0, joten ScanlineByteCount on 0, joten SetLength(PreviousScanline, 0) -kutsua seuraa välittömästi FillChar(PreviousScanline[0], ScanlineByteCount, 0). Tyhjän dynaamisen taulukon nollannen alkion indeksointi on nil-arvosta laskettu osoite. Kun alueentarkistus on pois päältä, nollatavuinen täyttö tuon osoitteen kautta on äänetön no-op ja dekooderi marssii eteenpäin rivien läpi, joita ei ole olemassa; kun alueentarkistus on päällä, se on ERangeError ensimmäisellä kuvalla; ja sitä seuraavat Move-kutsut ovat askeleen päässä access violation -virheestä. Se, minkä noista saat, riippuu kääntäjästä ja käännösvivustoista eikä mistään, mitä dekooderi päätti, ja juuri se paljastaa, ettei dekooderi päättänyt yhtään mitään

Korjaus on spesifikaation taulukko, sovellettuna sinne, missä muita IHDR-kenttiä jo tarkistettiin: COLOR_GRAYSCALE hyväksyy FSourceBitDepth in [1, 2, 4, 8, 16], COLOR_PALETTE hyväksyy [1, 2, 4, 8] ja COLOR_RGB, COLOR_GRAYSCALEALPHA ja COLOR_RGBALPHA hyväksyvät [8, 16]; kaikki muu nollaa ValidImage-lipun ja kuva hylätään sen leveyden ja korkeuden säilyessä diagnostiikkaa varten. Yhdeksää tavua lyhyempi pHYs-lohko korjattiin samalla kertaa, koska DPI-lukija indeksoi S[1]–S[8] merkkijonosta, jonka lyhyt lohko oli jättänyt tyhjäksi

1-pohjainen siirtymä 0-pohjaisena osoittimena

InflateStrFromPosition(Const Input: AnsiString; StartPos, MaxOutput: Integer; Out Consumed: Integer): AnsiString ottaa 1-pohjaisen StartPos-arvon, koska sen syöte on AnsiString ja Delphin toteutus osoittaa zlib-syötettä muodossa @Input[StartPos]. Free Pascal -toteutus, joka kirjoitettiin paszlib-kirjaston päälle, jotta molemmat Windows-kohteet linkittävät pakkauksen staattisesti, asetti next_in-arvoksi PAnsiChar(Input) + StartPos ja avail_in-arvoksi Length(Input) - StartPos. Se on osoitinaritmetiikkaa, ja se on 0-pohjaista. Anna arvoksi 1, mikä on tälle funktiolle se mitä ”aloita alusta” tarkoittaa, niin FPC-käännös alkaa purkaa pakkausta toisesta tavusta ja pysähtyy yhtä tavua ennen loppua

Se, miksi vika selvisi hengissä, johtuu siitä, että ainoa kutsuja, johon useimmat testit yltävät, on InflateStr, joka välittää 0:n. Nolla sattuu olemaan oikea 0-pohjainen siirtymä, joten molemmat käännökset olivat samaa mieltä jokaisesta tavallisesta InflateStr-kutsusta ja jokaisesta sen kautta kulkeneesta testistä. TPDFDocument.DecodeAllStreams, rutiini jota SaveQDFToFile ja ConvertFileToQDF käyttävät yhden FlateDecode-suodattimen streamien purkamiseen luettavaan muotoon, välittää 1:n. FPC-käännöksessä ohitettu zlib-otsikko sai purkamisen epäonnistumaan, mutta zlib-stream ilmoitti silti nollasta poikkeavan Consumed-arvon tutkimilleen tavuille, joten DecodeAllStreams tulkitsi tyhjän sisällön onnistuneeksi purkamiseksi ja korvasi jokaisen sisältöstreamin tyhjällä merkkijonolla. Syntynyt QDF oli oikean sivumäärän omaava, rakenteellisesti kelvollinen eikä sisältänyt yhtään sivunsisältöä, eli tiedosto, joka avautuu virheettä jokaisessa katselimessa ja näyttää tyhjää

// InflateStrFromPosition-funktion FPC-haara, version v3.539.16 jälkeen.
// StartPos on 1-pohjainen kuten Delphi-haarassa; rajataan se ja
// muunnetaan 0-pohjaiseksi osoitinsiirtymäksi tasan kerran, rajalla.
If (StartPos < 1) Then
  StartPos := 1;
If (Length(Input) = 0) Or (StartPos > Length(Input)) Then
  Exit;
...
strm.next_in  := Pointer(PAnsiChar(Input) + StartPos - 1);
strm.avail_in := Length(Input) - StartPos + 1;

Sitä vartioiva regressiotesti on pienin mahdollinen: pakkaa hyötykuorma deflate-muotoon, pura se sijainnista 0 ja sijainnista 1 ja varmista, että molemmat palauttavat saman hyötykuorman ja ilmoittavat Consumed-arvoksi koko streamin pituuden. RFC 1950 -streamissa on kaksitavuinen otsikko ja nelitavuinen Adler-32-perävaunu, joten yhden pykälän heitto kummassa tahansa päässä ei ole hienovarainen turmeltuma, vaan streami, joka joko ei käynnisty tai ei pääty. Oppi koskee rajaa, ei zlibiä: kun funktion parametri on määritelty yhdessä indeksikannassa ja alla oleva toteutus käyttää toista, muunnos kuuluu tasan yhdelle riville, ja testin on kutsuttava sitä arvolla, joka erottaa kannat toisistaan

Miksi vajaa TStream.Read ei tarkoita streamin loppua?

Koska TStream.Read saa palauttaa pyydettyä vähemmän tavuja mistä tahansa syystä, ja vain 0:n palautus tarkoittaa, ettei mitään enää ole. Paikallisella levyllä olevat TMemoryStream ja TFileStream täyttävät pyynnön lähes aina, minkä takia koodi, joka tulkitsee ”palautti vähemmän kuin pyysin” tiedoston lopuksi, läpäisee jokaisen niitä käyttävän testin. Verkossa olevat streamit, pakkauksen purkavat streamit ja mikä tahansa asiakkaan kirjoittama TStream-jälkeläinen voivat palauttaa kaksi tavua, kun pyydetään kuusikymmentäneljätuhatta, ja niiden takana voi silti olla gigatavuja

TPLBuffer on lukija, jonka kautta jokainen PDF Library for Delphin jäsennin kulkee, ja se voi kääriä sisäänsä AnsiString-merkkijonon, osoittimen, tavutaulukon tai TStream-streamin. Sen neljä skannauskyselyä, DistanceToByte, DistanceToOtherByte, DistanceToAnyByte ja DistanceToOtherBytes, kaikki palauttaen Int64-arvon, lukevat lähdettä 64 kilotavun lohkoina etsien erotinta ja ilmoittavat sen etäisyyden siirtämättä loogista sijaintia. Jokainen luuppi päättyi ehtoon Until ReadCount < BlockSize. Kolmelle muistissa olevalle lähteelle se on oikein, sillä ReadIntoBuffer toimittaa aina täyden lohkon viimeistä lukuun ottamatta. Stream-lähteelle se tarkoittaa, että skannaus luovuttaa ensimmäisestä vajaasta luvusta, ilmoittaa erottimen puuttuvaksi, ja sen yläpuolella oleva tokenisoija päättää objektin päättyvän sinne, missä se ei pääty

Vajaiden lukujen käsittely PDFlibPasin streamipuskurissa: DistanceToByte skannaa 64 kilotavun lohkoja, vanha luuppi tulkitsi ehdon Until ReadCount < BlockSize datan lopuksi ja luovutti ensimmäisestä vajaasta luvusta, kun taas korjattu luuppi jatkaa kunnes ReadCount on nolla, löytää erottimen ja palauttaa sijainnin finally-lohkossa
Streami voi palauttaa kaksi tavua, kun pyydetään kuusikymmentäneljätuhatta, joten nolla on ainoa datan lopun merkki, johon skannaus voi luottaa, ja finally-lause palauttaa loogisen sijainnin, kun erotin löytyy ja luuppi poistuu kesken
// TPLBuffer.DistanceToByte, luuppi version v3.539.6 jälkeen.
// Nolla on ainoa datan lopun merkki, jonka TStream.Read määrittelee.
TempPosition := FPosition;
Try
  Repeat
    ReadCount := ReadIntoBuffer(@TempBuffer[0], BlockSize);
    For TestPos := 0 To ReadCount - 1 Do
      If TempBuffer[TestPos] = Value Then
      Begin
        Result := TotalSkipped + TestPos;
        Exit;
      End;
    Inc(TotalSkipped, ReadCount);
  Until ReadCount = 0;
Finally
  FPosition := TempPosition;   // kurkistus ei saa siirtää lukijaa
End;

Tämän kiinnittävä testi on TMemoryStream-jälkeläinen, jonka Read-ylikirjoitus rajoittaa jokaisen pyynnön kahteen tavuun. Kääri sen sisään merkkijono aaaaaX, aseta puskurin sijainniksi 1, niin kaikkien neljän kyselyn on ilmoitettava etäisyydeksi 4 X-merkkiin, jätettävä sijainniksi 1 sen jälkeen ja palautettava -1 tavulle, jota ei ole. Ennen korjausta ensimmäinen kysely näki kaksi tavua, päätteli streamin loppuneen ja palautti -1. finally on yhtä tärkeä kuin luupin ehto: skannauksen sisältä tuleva Exit on normaali onnistumispolku, ja looginen sijainti on palautettava myös tuolla polulla, ei vain silloin kun luuppi pyörii loppuun asti

Yksi lähdekoodi, kaksi kääntäjää, yksi väitteiden joukko

Kurinpito, joka näistä viidestä syntyi, on se, että ”Delphin käännös läpäisee testit” on todiste Delphistä, ei lähdekoodista. Versiosta v3.539.16 lähtien sekä Delphin DUnitX-sarja että Free Pascalin konsolisarja sisältävät saman Tests\CrossCompilerSemantics.inc-tiedoston, yhden rutiinin RunCrossCompilerFileSemantics, joka rakentaa TPDFlib-rajapinnan kautta kaksisivuisen asiakirjan pakattua sisältöä käyttäen, tallentaa sen, tallentaa sen uudelleen QDF-muotoon SaveQDFToFile-kutsulla, korjaa QDF:n RepairQDFFile-kutsulla, salaa tavallisen tiedoston AES-128:lla EncryptFile-kutsulla ja EncodePermissions-funktiosta saadulla oikeusmaskilla ja lataa sitten jokaisen tuotoksen uudelleen ja varmistaa molemmilla kääntäjillä samat asiat: sivumäärä on 2, otsikko säilyy, toisen sivun teksti saadaan purettua ehjänä tavallisesta, korjatusta ja salatusta tiedostosta, väärä salasana hylätään nollasta poikkeavalla LastErrorCode-arvolla, EncryptionStrength on 128, EncryptionAlgorithm on 2 ja GetUserPermissions-funktion yksittäiset oikeusbitit palaavat täsmälleen koodattuina

Vertailu on tarkoituksella normalisoitua eikä tavu tavulta -vertailua. Salaus arpoo satunnaiset suolat ja kirjoittaja määrää asiakirjatunnisteet, joten kahden käännöksen ei odoteta tuottavan identtisiä tiedostoja; niiden odotetaan tuottavan tiedostoja, jotka tarkoittavat samaa asiaa, ja väitteet on muotoiltu tuolla tasolla. QDF-osuus on mukana nimenomaan siirtymävirheen takia: QDF, jossa on kaksi sivua eikä yhtään sisältöä, läpäisee sivumäärätarkistuksen ja kaatuu tekstinpoimintatarkistukseen, ja testimatriisi vaatii jälkimmäisen. Jokaisen tulevan korjauksen, joka on no-op yhdellä kääntäjällä ja käytösmuutos toisella, mikä kuvaa neljää viidestä yllä olevasta, on nyt selvittävä samoista väitteistä kahdesti ennen kuin se julkaistaan

Saman siirron linkitysaikainen puolisko, Delphin OMF-objektien ja Free Pascalin COFF-odotusten saaminen sopuun, on oma tarinansa artikkelissa FPC Win32 OMF- ja COFF-objektien linkityksestä, ja saman TIFF-lukijan rakenteellinen koventaminen BigTIFF- ja laatoitettuja tiedostoja vastaan on käsitelty sisäänrakennetun TIFF-dekooderin muistiinpanoissa. Tämän artikkelin dekooderit ja niiden alla nykyään oleva kääntäjien välinen testi toimitetaan PDF Library for Delphissä Delphiä, C++Builderia ja Free Pascalia varten, missä saman lähdekoodin odotetaan ansaitsevan saman tuloksen jokaisella kohdekääntäjällä sen sijaan että yksi niistä myöntäisi sen