Före v3.539.24 komprimerade Free Pascal-bygget av PDF Library for Delphi stora strömmar i 64 KB-chunks och gav varje chunk egen zlib-header och checksumma, så en bilaga på 1 MiB blev sexton zlib-medlemmar limmade ände mot ände. ISO 32000-1 FlateDecode förväntar sig exakt en zlib-ström, och en standardavkodare stannar vid den första slutmarkören, vilket betyder att allt efter de första 65 536 bytena försvann i tysthet. Fixen håller ett zlib-tillstånd vid liv genom varje chunk i både DeflateStream och InflateStream
Buggen fanns bara på FPC-sidan av uniten, och bara på den chunkade strömningsvägen, vilket är precis varför den överlevde: Delphi-grenen var alltid korrekt, de strängbaserade hjälpfunktionerna var alltid korrekta, och små testpayloads nådde aldrig den chunkade koden alls. Den är nära släkt med felen i fem FPC-portningsbuggar som Delphi dolde, fast här dolde Delphi-bygget ingenting. FPC-grenen hade helt enkelt skrivits utifrån en felaktig föreställning om vad en zlib-ström är
Varför avkortas en chunkad zlib-ström av andra PDF-läsare?
Därför att en zlib-ström enligt RFC 1950 är en enda behållare, inte en sekvens av behållare, och en regelkonform inflater behandlar den första Adler-32-trailern som slutet på datan. Formatet är en tvåbyte-header, en sammanhängande deflate-bitström enligt RFC 1951 vars sista block bär final-block-flaggan, och en fyrabyte Adler-32-checksumma över alla okomprimerade byte. ISO 32000-1 §7.4.4 definierar /FlateDecode i exakt dessa termer. När inflate når trailern returnerar den Z_STREAM_END och lämnar eventuell återstående indata oläst i avail_in. Ur dess synvinkel är ingenting fel, så den kastar inget fel, och bytena efter trailern ignoreras helt enkelt. Sammanfogade medlemmar är en legitim idé i gzip (RFC 1952 tillåter flera medlemmar i en fil), och det är förmodligen där intuitionen kom ifrån, men zlib har ingen sådan regel och PDF bad aldrig om en
Den gamla FPC-DeflateStream läste källan 64 KB i taget och lämnade varje chunk till ZFPCCompress, en hjälpfunktion som kör sin egen deflateInit2, komprimerar med Z_FINISH och anropar deflateEnd. Varje chunk blev alltså en komplett, giltig, självavslutad zlib-ström, och funktionen sammanfogade dem till en AnsiString innan den skrev ut resultatet. Resultatet såg ut som Flate-data, hade en korrekt header och avkodades utan fel, men det avkodades bara till de första 65 536 bytena. På läsarsidan hade InflateStream spegelbildsdefekten: den anropade ZFPCInflate en gång per 64 KB komprimerad indata, och varje anrop startade en ny inflateInit2. Den andra chunken börjar mitt i en deflate-bitström utan zlib-header, så en ny inflater avvisar den, och en helt normal ström med en enda medlem från valfri annan producent avkodades bara så långt som dess första 64 KB komprimerade byte räckte
// FPC:s DeflateStream före v3.539.24 (förenklad):
// ZFPCCompress kör deflateInit2 / deflate(Z_FINISH) / deflateEnd,
// så varje 64 KB-chunk blir en separat zlib-medlem
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));
Vilka PDF Library for Delphi-anrop nådde den chunkade vägen?
På FPC skrevs varje inbäddad fil på 1 MiB eller mer fel, och varje Flate-ström som extraherades genom strömmande API lästes fel så snart dess komprimerade storlek passerade en chunk. TPDFStream.ReadFromStream avgör hur inkommande data kodas. När Deflate är true och Stream.Size >= 1048576 strömmas data genom DeflateStream; under den tröskeln läses hela källan in i minnet och DeflateStr anropas, hjälpfunktionen som sköter allt i ett svep och aldrig drabbades. Filterkedjan ASCII85 plus Flate hoppar över storlekstestet och går alltid genom DeflateStream, så på den vägen var redan varje payload större än 64 KB uppdelad i flera medlemmar. De publika ingångspunkterna som matar ReadFromStream med komprimering påslagen är bilageskrivarna:
TPDFlib.EmbedFileochTPDFlib.AddEmbeddedFile, som läser en fil från disk till en/EmbeddedFile-strömTPDFlib.AddAssociatedFileFromStreamochTPDFlib.AddAssociatedFileFromFile, PDF/A-3-skrivarna för associerade filer som används för e-faktura-XML och annan källdata- På läsarsidan
TPDFlib.GetEmbeddedFileContentToStreamochGetEmbeddedFileContentToFile, som avkodar genomTPDFStream.WriteDecodedToStreamoch därifrån genomInflateStream
Felet var tyst i varje lager. Skrivaren lagrar /Params /Size och en MD5-/CheckSum beräknad ur originalfilen, så bilageordboken utlovade hela storleken medan strömmen innehöll sexton medlemmar. Ett Delphi-bygge av samma bibliotek som läste den filen stannade rent vid första Z_STREAM_END och returnerade exakt 65 536 byte. GetEmbeddedFileContentToStream returnerade 1, eftersom den rapporterar om avkodningen kastade ett fel, inte om utdatan matchar /Size. Den som någon gång jagat ett problem med stora dokument genom att slå ihop och dela gigabyte-PDF:er känner igen mönstret: filen öppnas, sidantalet stämmer, och skadan syns först när någon öppnar bilagan
Ett deflate-tillstånd genom varje chunk
Den fixade DeflateStream i PDFlibZLib.pas initierar en paszlib.TZStream, matar varje chunk till deflate med Z_NO_FLUSH och tömmer först på slutet kompressorn med Z_FINISH tills den returnerar Z_STREAM_END. Det ger exakt en header, en deflate-bitström vars bakåtreferenser kan sträcka sig över chunkgränser, och en Adler-32 över hela indatan. FPC-grenen har nu samma struktur som Delphi-grenen alltid haft. Den skriver också ut data allt eftersom den produceras, i stället för att först samla hela det komprimerade resultatet i en AnsiString, så skrivaren inte längre bygger en andra fullständig kopia av den komprimerade datan i minnet innan den kopieras till målet
// FPC:s DeflateStream från v3.539.24 (felsökvägar borttagna)
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); // samma tillstånd, inget medlemsbrott
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 // en trailer för hela indatan
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 fick den symmetriska omskrivningen: en inflateInit2, en inre loop som fortsätter anropa inflate tills den aktuella chunken är konsumerad och utdatabufferten inte längre är full, och ett stopp vid Z_STREAM_END. Det finns en bieffekt värd att känna till. Den gamla chunkade skrivaren hårdkodade nivå 6, medan den nya respekterar PLDeflateLevel, så en nivå satt genom TPDFlib.SetCompressionLevel(1..9) gäller nu stora inbäddade filer på FPC också. Det spelar roll om du redan trimmar komprimeringen för arkivutdata som beskrivs i att minska PDF-filstorleken i Delphi
Hur kontrollerar du att en Flate-ström är en enda zlib-medlem?
Inflatera den med en enkel zlib-avkodare och kontrollera två saker när den returnerar Z_STREAM_END: den avkodade längden är densamma som källans, och avail_in är noll. Indata som är kvar efter slutmarkören är signumet på en sammanfogad ström. Fixen verifierades på det sättet: 200 KB testdata, som spänner över fyra 64 KB-chunks, gick genom den nya DeflateStream och kom ut som en enda zlib-ström på 534 byte, en standard zlib-avkodare återvann alla 200 000 byte utan återstående indata, och samma kontroll gick igenom på det korskompilerade i386-målet för FPC. Rutinen nedan är FPC-versionen av den kontrollen, byggd direkt på paszlib så att den inte litar på koden som testas
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;
// En medlem slutar exakt vid sista indatabyte
Result:= (Status = Z_STREAM_END) and (strm.avail_in = 0);
finally
inflateEnd(strm);
end;
end;
// Skicka 200 000 byte genom DeflateStream med standardchunkstorleken 64 KB
// och kräv en enda medlem som avkodas tillbaka till full längd
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');
På applikationsnivå är det användbara påståendet det biblioteket inte gör åt dig: jämför vad som kommer ut ur en bilaga med den /Params /Size som registrerades när den gick in. GetEmbeddedFileIntProperty med tagg 5 returnerar den registrerade storleken, bilageindex är 1-baserade, och payloaden måste vara minst 1 MiB för att strömningsvägen ska användas. Kör samma test på varje kompilator du levererar med, eftersom den ursprungliga defekten gick bra på Delphi och misslyckades bara på FPC
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');
// 1 MiB eller mer tar den chunkade DeflateStream-vägen i ReadFromStream
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); // sparad /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;
Vad ändrar fixen inte?
FPC-grenen behåller sina toleranta avkodningsregler, och den reparerar inte filer som tidigare FPC-byggen redan skrivit. När MaxOutput nås avkortar FPC-InflateStream vid gränsen och returnerar, medan Delphi-grenen kastar ERangeError, och FPC accepterar fortfarande delvis avkodad utdata när inflate rapporterar datafel, eftersom vissa PDF-producenter skickar ut avkortade strömmar eller strömmar med bruten checksumma. En PDF skriven av ett FPC-bygge äldre än v3.539.24 innehåller fortfarande sammanfogade medlemmar, och den korrigerade läsaren stannar, som alla andra läsare, vid första Z_STREAM_END. Försök inte bota en sådan fil genom att avkoda och koda om strömmen inuti biblioteket, för det gör bara 64 KB-avkortningen permanent. Bädda in bilagan igen från sin ursprungliga källa i stället. FPC-loopen slutar också fortfarande vid första läsning som returnerar mindre än en hel chunk, vilket bara är en slut-på-data-signal för strömmar som TFileStream och TMemoryStream, så en egen TStream som skickas till AddAssociatedFileFromStream är säkrast att kopiera till en TMemoryStream först. Delphi-grenen, hjälpfunktionerna DeflateStr och InflateStr och varje ström mindre än 1 MiB på den vanliga Flate-vägen beter sig exakt som tidigare
De korrigerade FPC-DeflateStream och InflateStream kommer i v3.539.24 av PDF Library for Delphi, som riktar sig till Delphi, C++Builder och Free Pascal från ett och samma källträd, och där en stor bilaga nu ska komma tillbaka från ett FPC-bygge byte för byte, på samma sätt som den alltid gjort från Delphi