Ennen versiota v3.539.24 PDF Library for Delphin Free Pascal -kooste paketoi suuret streamit 64 kilotavun paloissa ja antoi jokaiselle palalle oman zlib-otsikon ja tarkistussumman, joten 1 MiB:n liitetiedostosta tuli päistikkaan liimattujen zlib-jäsenten ketju. ISO 32000-1:n FlateDecode odottaa täsmälleen yhtä zlib-streamia, ja standardinmukainen dekooderi pysähtyy ensimmäiseen streamin lopun merkkiin, eli kaikki ensimmäisten 65 536 tavun jälkeen katosi hiljaa. Korjaus pitää yhden zlib-tilan elossa jokaisen palan yli sekä DeflateStreamissa että InflateStreamissa
Bugi asusteli vain yksikön FPC-puolella ja vain paloitellulla streamauspolulla, ja täsmälleen siksi se selviytyi: Delphi-haara toimi aina oikein, merkkijonopohjaiset apurit toimivat aina oikein, eikä pienet testikuormat koskaan ylettäneet paloiteltuun koodiin saakka. Vika on läheistä sukua viidelle FPC-siirtobugille, jotka Delphi oli pitänyt piilossa, paitsi että tässä Delphi-kooste ei suojellut mitään. FPC-haara oli yksinkertaisesti kirjoitettu väärän mielikuvan varaan siitä, mitä zlib-stream oikeastaan on
Miksi muut PDF-lukijat katkaisevat paloitellun zlib-streamin?
Siksi, että RFC 1950:n määrittelemä zlib-stream on yksi kontti, ei niiden sarja, ja standardinmukainen inflater tulkitsee ensimmäisen Adler-32-loppusumman datan lopuksi. Muoto koostuu kaksitavuisesta otsikosta, yhtenäisestä RFC 1951:n deflate-bittivirrasta, jonka viimeinen lohko kantaa viimeistä lohkoa merkitsevää lippua, ja nelitavuisesta Adler-32-tarkistussummasta kaikkien pakkamattomien tavujen yli. ISO 32000-1 §7.4.4 määrittelee /FlateDecode-suodattimen juuri noin. Kun inflate saavuttaa loppusumman, se raportoi Z_STREAM_END-tilan ja jättää mahdollisen jäljelle jäävän syötteen lukematta avail_in-kenttään. Sen näkökulmasta mikään ei ole vialla, joten se ei nosta virhettä, ja loppusumman jälkeiset tavut yksinkertaisesti ohitetaan. Ketjutetut jäsenet ovat gzipissä ihan laillinen idea (RFC 1952 sallii useita jäseniä yhdessä tiedostossa), ja tuskin intuitio on tullut mistään muualta, mutta zlibillä ei ole sellaista sääntöä, eikä PDF koskaan pyytänytkään
Vanha FPC:n DeflateStream luki lähdestreamia 64 kilotavua kerrallaan ja antoi jokaisen palan ZFPCCompress-apurille, joka ajaa oman deflateInit2-alustuksen, pakkaa Z_FINISH-lipulla ja lopettaa deflateEnd-kutsuun. Jokainen pala tuli siis ulos täytenä, kelvollisena ja itseensä päättyvänä zlib-streamina, ja funktio liitti palat yhdeksi AnsiString-merkkijonoksi ennen kirjoittamista. Tulos näytti Flate-datalta, siinä oli oikea otsikko, ja se purkautui ilman virhettä, mutta vain ensimmäisiin 65 536 tavuun. Lukupuolella InflateStreamissa oli peilikuvasama vika: se kutsui ZFPCInflate-apuria kerran jokaista 64 kilotavua pakattua syötettä kohden, ja jokainen kutsu käynnisti tuoreen inflateInit2-alustuksen. Toinen pala alkaa deflate-bittivirran keskeltä ilman zlib-otsikkoa, joten uusi inflater hylkää sen, ja toiselta tuottajalta saapunut täysin normaali yhden jäsenen stream purkautui vain niin pitkälle kuin sen ensimmäiset 64 kilotavua pakattuja tavuja ulottuivat
// FPC:n DeflateStream ennen v3.539.24:ää (yksinkertaistettu):
// ZFPCCompress ajaa deflateInit2 / deflate(Z_FINISH) / deflateEnd,
// joten jokainen 64 kilotavun pala muuttuu erilliseksi zlib-jäseneksi
Repeat
ReadCount:= Source.Read(Input[1], ChunkSize);
If (ReadCount> 0) Then
Compressed:= Compressed+ ZFPCCompress(Copy(Input, 1, ReadCount), 6, False);
Until (ReadCount< ChunkSize);
If (Compressed<> '') Then
Target.WriteBuffer(PAnsiChar(Compressed)^, Length(Compressed));
Mitkä PDF Library for Delphin kutsut päätyivät paloitellulle polulle?
FPC:llä mikä tahansa vähintään 1 MiB:n upotettu tiedosto kirjoittui väärin, ja mikä tahansa streaming-API:n kautta purettu Flate-stream luettiin väärin heti kun sen pakattu koko ylitti yhden palan. TPDFStream.ReadFromStream päättää, miten saapuva data koodataan. Kun Deflate on tosi ja Stream.Size >= 1048576, data kulkee DeflateStreamin läpi; kynnyksen alla koko lähde luetaan muistiin ja kutsutaan DeflateStr-apuria, yksittäislaukauksen apuria, jota vika ei koskaan koskettanut. ASCII85:n ja Faten yhdistävä suodatinaketju hyppää kokotestin yli ja menee aina DeflateStreamin kautta, joten sillä polulla mikä tahansa yli 64 kilotavun kuorma oli jo jaettu useiksi jäseniksi. Julkiset sisääntulot, jotka syöttävät ReadFromStreamia pakkaus päällä, ovat upotettujen tiedostojen kirjoittajat:
TPDFlib.EmbedFilejaTPDFlib.AddEmbeddedFile, jotka lukevat tiedoston levyltä/EmbeddedFile-streamiinTPDFlib.AddAssociatedFileFromStreamjaTPDFlib.AddAssociatedFileFromFile, PDF/A-3:n liitetiedostojen kirjoittajat sähkölaskun XML:lle ja muille lähdetiedoille- Lukupuolella
TPDFlib.GetEmbeddedFileContentToStreamjaGetEmbeddedFileContentToFile, jotka purkavatTPDFStream.WriteDecodedToStreamin kautta ja sieltä edelleenInflateStreamin kautta
Virhe oli hiljainen joka kerroksessa. Kirjoittaja tallentaa /Params /Size-koon ja alkuperäisestä tiedostosta lasketun MD5-/CheckSumin, joten liitteen sanakirja mainosti täyttä kokoa samalla kun streamissä oli kuusitoista jäsentä. Saman kirjaston Delphi-kooste luki kyseisen tiedoston, pysähtyi siististi ensimmäiseen Z_STREAM_END-merkkiin ja palautti tarkalleen 65 536 tavua. GetEmbeddedFileContentToStream palautti 1, koska se raportoi, nousiko purusta virhettä, eikä sitä, täsmääkö tuotos /Sizeen. Jokainen, joka on jahdannut suuren asiakirjan ongelmaa gigatavujen PDFien yhdistämisen ja pilkkomisen kautta, tuntee tämän kaavan: tiedosto avautuu, sivumäärä on oikein, ja vahinko näkyy vasta kun joku avaa liitteen
Yksi deflate-tila jokaisen palan yli
Korjattu DeflateStream tiedostossa PDFlibZLib.pas alustaa yhden paszlib.TZStream-rakenteen, syöttää jokaisen palan deflate-funktioon Z_NO_FLUSH-lipulla ja vasta lopussa tyhjentää pakkaajan Z_FINISH-lipulla, kunnes paluuarvona on Z_STREAM_END. Siitä syntyy täsmälleen yksi otsikko, yksi deflate-bittivirta, jonka takaviittaukset ylettävät palojen rajojen yli, ja yksi Adler-32 koko syötteestä. FPC-haaralla on nyt sama rakenne, jota Delphi-haara on aina käyttänyt. Se kirjoittaa myös tuotosta ulos sitä mukaa kun se syntyy sen sijaan, että ensin kokoaisi koko pakatun tuloksen AnsiString-merkkijonoon, joten kirjoittaja ei enää rakenna toista täyttä kopiota pakatusta datasta muistiin ennen kuin kopioi sen kohteeseen
// FPC:n DeflateStream versiosta v3.539.24 alkaen (virhepolut karsittu)
If (deflateInit2(strm, Level, Z_DEFLATED, 15, 8, Z_DEFAULT_STRATEGY)<> Z_OK) Then
Exit;
Try
Repeat
ReadCount:= Source.Read(Input[0], ChunkSize);
If (ReadCount> 0) Then
Begin
strm.next_in:= Pointer(Input);
strm.avail_in:= ReadCount;
While (strm.avail_in> 0) Do
Begin
strm.next_out:= Pointer(Output);
strm.avail_out:= ChunkSize;
Status:= deflate(strm, Z_NO_FLUSH); // sama tila, ei jäsenkatkoa
Produced:= ChunkSize- strm.avail_out;
If (Produced> 0) Then
Target.WriteBuffer(Output[0], Produced);
If (Status<> Z_OK) Then
Break;
End;
End;
Until (ReadCount< ChunkSize);
Repeat // yksi loppusumma koko syötteelle
strm.next_out:= Pointer(Output);
strm.avail_out:= ChunkSize;
Status:= deflate(strm, Z_FINISH);
Produced:= ChunkSize- strm.avail_out;
If (Produced> 0) Then
Target.WriteBuffer(Output[0], Produced);
Until (Status= Z_STREAM_END);
Finally
deflateEnd(strm);
End;
InflateStream sai symmetrisen uudelleenkirjoituksen: yksi inflateInit2-alustus, sisäsilmukka, joka kutsuu inflateia kunnes nykyinen pala on kulutettu ja tuotosjono ei ole enää täynnä, ja pysähdys Z_STREAM_END-merkkiin. Yksi sivuvaikutus kannattaa tietää. Vanha paloitteleva kirjoittaja kovakoodasi tason 6, uusi noudattaa PLDeflateLevel-asetusta, joten TPDFlib.SetCompressionLevel(1..9) -kutsulla asetettu taso koskee nyt suuria upotettuja tiedostoja myös FPC:llä. Sillä on väliä, jos olet jo hienosäätänyt pakkausta arkistotuotosta varten niin kuin kerrotaan artikkelissa PDF-tiedoston koon pienentämisestä Delphissä
Miten varmistat, että Flate-stream on yksi zlib-jäsen?
Pura se tavallisella zlib-dekooderilla ja tarkista kaksi asiaa, kun paluuarvona on Z_STREAM_END: puretun datan pituus on lähdepituus ja avail_in on nolla. Loppumerkin jälkeen jäänyt syöte on ketjutetun streamin tunnusmerkki. Korjaus varmistettiin näin: 200 kilotavun testidata, joka kattaa neljä 64 kilotavun palaa, kulki uuden DeflateStreamin läpi ja tuli ulos 534-tavuisena yhtenä zlib-streamina, vakio-zlib-dekooderi sai kaikki 200 000 tavua takaisin ilman jäljelle jäänyttä syötettä, ja sama tarkistus meni läpi i386-ristikäännetyllä FPC-kohteella. Alla oleva rutiini on kyseisen tarkistuksen FPC-versio, rakennettu suoraan paszlibin päälle, joten se ei luota testattavaan koodiin
uses Classes, SysUtils, paszlib, PDFlibZLib;
function IsSingleZlibMember(Packed: TMemoryStream; out Decoded: Int64): Boolean;
var
strm: TZStream;
Buf: array[0..65535] of Byte;
Status: Integer;
begin
Result:= False;
Decoded:= 0;
FillChar(strm, SizeOf(strm), 0);
if inflateInit2(strm, 15) <> Z_OK then
Exit;
try
strm.next_in:= Packed.Memory;
strm.avail_in:= Cardinal(Packed.Size);
repeat
strm.next_out:= @Buf;
strm.avail_out:= SizeOf(Buf);
Status:= inflate(strm, Z_NO_FLUSH);
until Status <> Z_OK;
Decoded:= strm.total_out;
// Yksi jäsen päättyy täsmälleen viimeiseen syötetavuun
Result:= (Status = Z_STREAM_END) and (strm.avail_in = 0);
finally
inflateEnd(strm);
end;
end;
// Viedään 200 000 tavua DeflateStreamin läpi oletuskokoisella 64 kilotavun palalla
// ja vaaditaan yhtä jäsentä, joka purkautuu täyteen pituuteen
Source.Position:= 0;
DeflateStream(Source, Packed);
if not (IsSingleZlibMember(Packed, Decoded) and (Decoded = Source.Size)) then
raise Exception.Create('DeflateStream produced more than one zlib member');
Sovellustasolla hyödyllisin väite on se, jota kirjasto ei tee puolestasi: vertaa liitteen tuotosta sisäänkirjoitettuun /Params /Size-kokoon. GetEmbeddedFileIntProperty tunnisteella 5 palauttaa kirjatun koon, upotettujen tiedostojen indeksit alkavat ykkösestä, ja kuorman täytyy olla vähintään 1 MiB, jotta streamauspolku pääsee toimimaan. Aja sama testi jokaisella mukana toimitettavalla kääntäjällä, sillä alkuperäinen vika meni Delphillä läpi ja kaatui vain FPC:llä
procedure CheckLargeAttachmentRoundTrip(const PayloadFile, OutFile: string);
var
PDF: TPDFlib;
Extracted: TMemoryStream;
I, Declared: Integer;
begin
PDF:= TPDFlib.Create;
try
PDF.NewDocument;
PDF.AddStandardFont(4);
PDF.DrawText(80, 100, 'Large attachment round trip');
// Vähintään 1 MiB vie ReadFromStreamin paloitellulle DeflateStream-polulle
if PDF.EmbedFile('Payload', PayloadFile, 'application/octet-stream') <> 1 then
raise Exception.Create('EmbedFile failed');
if PDF.SaveToFile(OutFile) <> 1 then
raise Exception.Create('SaveToFile failed');
finally
PDF.Free;
end;
PDF:= TPDFlib.Create;
Extracted:= TMemoryStream.Create;
try
if PDF.LoadFromFile(OutFile, '') = 0 then
raise Exception.Create('LoadFromFile failed');
for I:= 1 to PDF.EmbeddedFileCount do
begin
Extracted.Clear;
if PDF.GetEmbeddedFileContentToStream(I, Extracted) <> 1 then
raise Exception.CreateFmt('Attachment %d could not be decoded', [I]);
Declared:= PDF.GetEmbeddedFileIntProperty(I, 5); // /Params /Size
if Extracted.Size <> Declared then
raise Exception.CreateFmt('Attachment %d truncated: %d of %d bytes',
[I, Extracted.Size, Declared]);
end;
finally
Extracted.Free;
PDF.Free;
end;
end;
Mitä korjaus ei muuta?
FPC-haara säilyttää sallivat purkusääntönsä, eikä se korjaa tiedostoja, joita aiemmat FPC-koosteet jo kirjoittivat. Kun MaxOutput täyttyy, FPC:n InflateStream katkaisee rajalla ja palaa, kun taas Delphi-haara nostaa ERangeError-poikkeuksen, ja FPC hyväksyy edelleen osittain purkatun tuotoksen, kun inflate raportoi datavirheestä, sillä jotkin PDF-tuottajat lähettävät katkenneita tai tarkistussummaltaan rikkoutuneita streameja. Ennen v3.539.24:tä olleen FPC-koosteen kirjoittama PDF sisältää edelleen ketjutettuja jäseniä, ja korjattu lukija pysähtyy ensimmäiseen Z_STREAM_END-merkkiin aivan kuten jokainen muukin lukija. Älä yritä parantaa sellaista tiedostoa purkamalla ja uudelleenkoodaamalla streamia kirjaston sisällä, sillä se vain tekee 64 kilotavun katkaisusta pysyvän. Upota liite sen sijaan uudelleen alkuperäisestä lähteestään. FPC-silmukka päättyy myös edelleen ensimmäiseen lukuun, joka palauttaa vajaan palan, ja se on datan lopun merkki vain TFileStreamin ja TMemoryStreamin kaltaisille streameille, joten AddAssociatedFileFromStreamille annettu oma TStream on turvallisinta kopioida ensin TMemoryStreamiin. Delphi-haara, DeflateStr- ja InflateStr-apurit sekä jokainen alle 1 MiB:n stream tavallisella Flate-polulla toimivat täsmälleen kuten ennenkin
Korjatut FPC:n DeflateStream ja InflateStream toimitetaan PDF Library for Delphin versiossa v3.539.24, joka tähtää Delphiin, C++Builderiin ja Free Pascaliin yhdestä lähdekoodipuusta, ja jossa suuren liitteen pitäisi nyt palata FPC-koosteelta tavu tavulta, aivan samoin kuin se on aina palannut Delphiltä