Vóór v3.539.24 comprimeerde de Free Pascal-build van PDF Library for Delphi grote streams in chunks van 64 KB en kreeg elke chunk zijn eigen zlib-header en checksum, zodat een bijlage van 1 MiB uit zestien achter elkaar geplakte zlib-members bestond. ISO 32000-1 verwacht bij FlateDecode precies één zlib-stream, en een standaarddecoder stopt bij de eerste end-of-stream-marker, wat betekent dat alles na de eerste 65.536 bytes geruisloos verdween. De fix houdt één zlib-state in leven over alle chunks heen, zowel in DeflateStream als in InflateStream
De bug bestond alleen in het FPC-deel van de unit, en alleen op het chunked streaming-pad, en dat is precies waarom hij kon overleven: de Delphi-tak was altijd correct, de stringgebaseerde helpers waren altijd correct, en kleine testpayloads bereikten de chunked code helemaal niet. Hij is een naaste verwant van de defecten uit vijf FPC-portagebugs die Delphi altijd had verborgen, met dit verschil dat de Delphi-build hier niets wegstopte. De FPC-tak was simpelweg geschreven vanuit een verkeerd beeld van wat een zlib-stream eigenlijk is
Waarom kappen andere PDF-readers een chunked zlib-stream af?
Omdat een zlib-stream zoals RFC 1950 die definieert één container is, geen reeks containers, en een conforme inflater de eerste Adler-32-trailer als het einde van de data behandelt. Het formaat bestaat uit een header van twee bytes, één doorlopende RFC 1951-deflate-bitstream waarvan het laatste blok de final-block-flag draagt, en een Adler-32-checksum van vier bytes over alle ongecomprimeerde bytes. ISO 32000-1 §7.4.4 definieert /FlateDecode precies in die termen. Zodra inflate de trailer bereikt geeft hij Z_STREAM_END terug en laat hij resterende invoer ongelezen in avail_in staan. Vanuit zijn perspectief is er niets aan de hand, dus hij geeft geen foutmelding, en de bytes na de trailer worden simpelweg genegeerd. Aaneengeregen members zijn een legitiem idee in gzip (RFC 1952 staat meerdere members in één bestand toe), en daar komt de intuïtie waarschijnlijk vandaan, maar zlib heeft zo'n regel niet en PDF heeft er nooit om gevraagd
De oude FPC-DeflateStream las de bron per 64 KB en gaf elke chunk door aan ZFPCCompress, een helper die zijn eigen deflateInit2 draait, met Z_FINISH comprimeert en deflateEnd aanroept. Elke chunk kwam er dus uit als een complete, geldige, zichzelf afsluitende zlib-stream, en de functie plakte ze aan elkaar tot één AnsiString voordat die weggeschreven werd. Het resultaat leek op Flate-data, had een correcte header en decodeerde zonder foutmelding, maar decodeerde wel tot precies de eerste 65.536 bytes. Aan de leeszijkant had InflateStream het spiegelbeeldige defect: hij riep ZFPCInflate aan per 64 KB gecomprimeerde invoer, en elke aanroep startte een verse inflateInit2. De tweede chunk begint midden in een deflate-bitstream zonder zlib-header, dus een nieuwe inflater wijst hem af, en een volstrekt normale single-member-stream van een willekeurige andere producent werd alleen zo ver gedecodeerd als zijn eerste 64 KB gecomprimeerde bytes reikten
// FPC-DeflateStream vóór v3.539.24 (vereenvoudigd):
// ZFPCCompress doet deflateInit2 / deflate(Z_FINISH) / deflateEnd,
// dus elke chunk van 64 KB wordt een apart zlib-member
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));
Welke aanroepen in PDF Library for Delphi bereikten het chunked pad?
Op FPC werd elk embedded file van 1 MiB of groter verkeerd weggeschreven, en werd elke via de streaming-API opgehaalde Flate-stream verkeerd gelezen zodra zijn gecomprimeerde grootte één chunk passeerde. TPDFStream.ReadFromStream bepaalt hoe inkomende data gecodeerd wordt. Is Deflate waar en geldt Stream.Size >= 1048576, dan streamt hij via DeflateStream; onder die drempel leest hij de hele bron in het geheugen en roept hij DeflateStr aan, de single-shot-helper die nooit last had. De filterketen ASCII85-plus-Flate slaat de sizetest over en gaat altijd via DeflateStream, dus op dat pad werd elke payload groter dan 64 KB al in meerdere members gesplitst. De publieke toegangspunten die ReadFromStream voeden met compressie ingeschakeld zijn de embedded-file-writers:
TPDFlib.EmbedFileenTPDFlib.AddEmbeddedFile, die een bestand van schijf inlezen in een/EmbeddedFile-streamTPDFlib.AddAssociatedFileFromStreamenTPDFlib.AddAssociatedFileFromFile, de PDF/A-3-associated-file-writers voor e-factuur-XML en andere brongegevens- Aan de leeszijkant
TPDFlib.GetEmbeddedFileContentToStreamenGetEmbeddedFileContentToFile, die decoderen viaTPDFStream.WriteDecodedToStreamen daarna viaInflateStream
De mislukking was in elke laag geruisloos. De writer bewaart /Params /Size en een MD5-/CheckSum berekend uit het originele bestand, dus de bijlage-dictionary adverteerde de volledige grootte terwijl de stream zestien members bevatte. Een Delphi-build van dezelfde library die dat bestand las stopte netjes bij de eerste Z_STREAM_END en gaf precies 65.536 bytes terug. GetEmbeddedFileContentToStream gaf 1 terug, want hij meldt of het decoderen een fout opleverde, niet of de uitvoer met /Size overeenkomt. Wie ooit een groot-documentenprobleem najoeg langs het samenvoegen en splitsen van gigabyte-PDF's kent dit patroon: het bestand opent, het paginanaantal klopt, en de schade komt pas boven water wanneer iemand de bijlage opent
Eén deflate-state over alle chunks heen
De herstelde DeflateStream in PDFlibZLib.pas initialiseert één paszlib.TZStream, voedt elke chunk aan deflate met Z_NO_FLUSH, en spoelt aan het einde de compressor pas leeg met Z_FINISH tot die Z_STREAM_END teruggeeft. Dat levert precies één header op, één deflate-bitstream waarvan de back-references over chunkgrenzen heen kunnen reiken, en één Adler-32 over de hele invoer. De FPC-tak heeft nu dezelfde structuur die de Delphi-tak altijd al had. Hij schrijft de uitvoer bovendien weg naarmate die ontstaat, in plaats van eerst het hele gecomprimeerde resultaat in een AnsiString aan elkaar te plakken, dus de writer bouwt geen tweede volledige kopie van de gecomprimeerde data meer in het geheugen voordat die naar het doel gekopieerd wordt
// FPC-DeflateStream vanaf v3.539.24 (foutpaden weggelaten)
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); // zelfde state, geen member-breuk
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 // één trailer voor de hele invoer
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 kreeg de symmetrische herschrijving: één inflateInit2, een binnenlus die inflate blijft aanroepen tot de huidige chunk verbruikt is en de uitvoerbuffer niet langer vol zit, en een stop op Z_STREAM_END. Er is één bijwerking die u moet kennen. De oude chunked writer hardcodeerde level 6, terwijl de nieuwe PLDeflateLevel volgt, dus een level dat via TPDFlib.SetCompressionLevel(1..9) gezet is geldt nu ook voor grote embedded files op FPC. Dat is relevant als u compressie al afstemt op archiefuitvoer, zoals beschreven in het verkleinen van PDF-bestanden in Delphi
Hoe verifieert u dat een Flate-stream één enkele zlib-member is?
Inflateer hem met een gewone zlib-decoder en controleer twee dingen op het moment dat die Z_STREAM_END teruggeeft: de gedecodeerde lengte is gelijk aan de bronnengte, en avail_in is nul. Invoer die na de eindmarker overblijft is de signatuur van een aaneengeregen stream. De fix is zo geverifieerd: 200 KB testdata, verspreid over vier chunks van 64 KB, ging door de nieuwe DeflateStream en kwam eruit als één zlib-stream van 534 bytes, een standaard zlib-decoder haalde alle 200.000 bytes terug zonder resterende invoer, en dezelfde controle slaagde op de i386-crossgecompileerde FPC-doelomgeving. De routine hieronder is de FPC-versie van die controle, rechtstreeks op paszlib gebouwd zodat hij de code die getest wordt niet vertrouwt
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;
// Eén member eindigt precies op de laatste invoerbyte
Result:= (Status = Z_STREAM_END) and (strm.avail_in = 0);
finally
inflateEnd(strm);
end;
end;
// Duw 200.000 bytes door DeflateStream met de standaard chunk van 64 KB
// en eis één member die terug decodeert tot de volledige lengte
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');
Op applicatieniveau is de nuttige assertie degene die de library niet voor u maakt: vergelijk wat eruit komt van een bijlage met de /Params /Size die bij het erin stoppen is vastgelegd. GetEmbeddedFileIntProperty met tag 5 geeft die vastgelegde grootte terug, embedded-file-indexen zijn 1-based, en de payload moet minstens 1 MiB zijn om het streamingpad te raken. Draai dezelfde test op elke compiler waar u mee uitlevert, want het oorspronkelijke defect bleef op Delphi onzichtbaar en trad alleen op FPC op
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 of meer neemt het chunked DeflateStream-pad in 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); // /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;
Wat verandert de fix niet?
De FPC-tak behoudt zijn soepele decoderingsregels, en hij herstelt geen bestanden die eerdere FPC-builds al geschreven hebben. Als MaxOutput bereikt is kapt FPC-InflateStream op de grens af en keert terug, terwijl de Delphi-tak een ERangeError gooit, en FPC accepteert nog steeds gedeeltelijk gedecodeerde uitvoer wanneer inflate een datafout meldt, omdat sommige PDF-producenten afgekapte of checksum-gebroken streams uitzenden. Een PDF geschreven door een FPC-build ouder dan v3.539.24 bevat nog steeds aaneengeregen members, en de gecorrigeerde reader stopt, net als elke andere reader, bij de eerste Z_STREAM_END. Probeer zo'n bestand niet te genezen door de stream binnen de library te decoderen en opnieuw te encoderen, want dat maakt de afkapping van 64 KB alleen maar permanent. Embed de bijlage opnieuw vanaf zijn oorspronkelijke bron. De FPC-lus eindigt bovendien nog steeds op de eerste leesactie die minder dan een volle chunk teruggeeft, wat alleen voor streams als TFileStream en TMemoryStream een end-of-data-signaal is, dus een eigen TStream die aan AddAssociatedFileFromStream wordt meegegeven kopieert u het veiligst eerst naar een TMemoryStream. De Delphi-tak, de helpers DeflateStr en InflateStr, en elke stream kleiner dan 1 MiB op het gewone Flate-pad gedragen zich precies zoals voorheen
De gecorrigeerde FPC-DeflateStream en InflateStream zitten in v3.539.24 van PDF Library for Delphi, die Delphi, C++Builder en Free Pascal vanuit één source tree bedient, en waar een grote bijlage nu byte voor byte terug moet komen uit een FPC-build, op dezelfde manier als hij dat altijd al uit Delphi deed