Før v3.539.24 komprimerte Free Pascal-bygget av PDF Library for Delphi store strømmer i 64 KB-biter og ga hver bit sin egen zlib-header og sjekksum, så et vedlegg på 1 MiB ble seksten zlib-medlemmer limt ende til ende. ISO 32000-1 FlateDecode forventer nøyaktig én zlib-strøm, og en standarddekoder stopper ved den første ende-av-strøm-markøren, noe som betyr at alt etter de første 65 536 bytene forsvant i stillhet. Fiksen holder én zlib-tilstand i live på tvers av alle biter i både DeflateStream og InflateStream
Feilen fantes bare på FPC-siden av uniten, og bare på den chunkede strømmingsstien, som er nøyaktig grunnen til at den overlevde: Delphi-grenen var alltid riktig, de strengbaserte hjelperne var alltid riktige, og små testdata nådde aldri frem til den chunkede koden i det hele tatt. Den er en nær slektning av feilene i fem FPC-porteringsfeil som Delphi hadde skjult, bare med den forskjell at Delphi-bygget her ikke dekket over noe. FPC-grenen var rett og slett skrevet ut fra en feil mental modell av hva en zlib-strøm er
Hvorfor blir en chunked zlib-strøm avkortet av andre PDF-lesere?
Fordi en zlib-strøm slik RFC 1950 definerer den, er én beholder, ikke en rekke av dem, og en regelrett inflater behandler den første Adler-32-traileren som slutten på dataene. Formatet er en header på to byte, én sammenhengende RFC 1951 deflate-bitstrøm der siste blokk bærer final-block-flagget, og en Adler-32-sjekksum på fire byte over alle ukomprimerte byte. ISO 32000-1 §7.4.4 definerer /FlateDecode i nøyaktig disse termene. Når inflate når traileren, returnerer den Z_STREAM_END og lar eventuelt gjenværende input ligge ulest i avail_in. Sett fra dens ståsted er ingenting galt, så den reiser ingen feil, og bytene etter traileren ignoreres rett og slett. Sammenkjedede medlemmer er en legitim idé i gzip (RFC 1952 tillater flere medlemmer i én fil), som sikkert er der intuisjonen kom fra, men zlib har ingen slik regel, og PDF har aldri bedt om én
Den gamle FPC-DeflateStream leste kilden 64 KB om gangen og ga hver bit videre til ZFPCCompress, en hjelper som kjører sin egen deflateInit2, komprimerer med Z_FINISH og kaller deflateEnd. Hver bit kom dermed ut som en komplett, gyldig, selvbegrenset zlib-strøm, og funksjonen kjedet dem sammen til én AnsiString før den ble skrevet ut. Resultatet så ut som Flate-data, hadde en korrekt header og dekodet uten feil, men det dekodet til de første 65 536 bytene, ikke mer. På lesesiden hadde InflateStream speilbildefeilen: den kalte ZFPCInflate én gang per 64 KB komprimert input, og hvert kall startet en fersk inflateInit2. Den andre biten begynner midt i en deflate-bitstrøm uten zlib-header, så en ny inflater avviser den, og en helt normal enkeltmedlemsstrøm fra enhver annen produsent ble dekodet bare så langt som de første 64 KB komprimerte byte bar den
// FPC DeflateStream før v3.539.24 (forenklet):
// ZFPCCompress kjører deflateInit2 / deflate(Z_FINISH) / deflateEnd,
// så hver 64 KB-bit blir et eget 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));
Hvilke PDF Library for Delphi-kall nådde den chunkede stien?
På FPC ble ethvert innebygget filvedlegg på 1 MiB eller mer skrevet feil, og enhver Flate-strøm hentet ut gjennom strømmings-API-et ble lest feil så snart den komprimerte størrelsen passerte én bit. TPDFStream.ReadFromStream avgjør hvordan innkommende data kodes. Når Deflate er sann og Stream.Size >= 1048576, strømmer den gjennom DeflateStream; under den terskelen leser den hele kilden inn i minnet og kaller DeflateStr, enkeltskudd-hjelperen som aldri ble berørt. Filterkjeden ASCII85 pluss Flate hopper over størrelsestesten og går alltid gjennom DeflateStream, så på den stien var allerede all nyttelast over 64 KB delt opp i flere medlemmer. De offentlige inngangspunktene som mater ReadFromStream med komprimering slått på, er skriverne for innebygde filer:
TPDFlib.EmbedFileogTPDFlib.AddEmbeddedFile, som leser en fil fra disk inn i en/EmbeddedFile-strømTPDFlib.AddAssociatedFileFromStreamogTPDFlib.AddAssociatedFileFromFile, PDF/A-3-skriverne for tilknyttede filer som brukes til e-faktura-XML og andre kildedata- På lesesiden
TPDFlib.GetEmbeddedFileContentToStreamogGetEmbeddedFileContentToFile, som dekoder gjennomTPDFStream.WriteDecodedToStreamog derfra gjennomInflateStream
Feilen var stille i hvert lag. Skriveren lagrer /Params /Size og en MD5-/CheckSum beregnet fra originalfilen, så vedleggsordboken annonserte full størrelse mens strømmen holdt seksten medlemmer. Et Delphi-bygg av samme bibliotek som leste den filen, stoppet pent ved den første Z_STREAM_END og returnerte nøyaktig 65 536 byte. GetEmbeddedFileContentToStream returnerte 1, fordi den rapporterer om dekoding reiste en feil, ikke om resultatet matcher /Size. Alle som har jaget et stort dokumentproblem gjennom sammenslåing og splitting av gigabyte-PDF-er, kjenner dette mønsteret: filen åpner seg, sideantallet stemmer, og skaden viser seg først når noen åpner vedlegget
Én deflate-tilstand på tvers av alle biter
Den reparerte DeflateStream i PDFlibZLib.pas initialiserer én paszlib.TZStream, mater hver bit til deflate med Z_NO_FLUSH, og først på slutten tapper den kompressoren med Z_FINISH til den returnerer Z_STREAM_END. Det gir nøyaktig én header, én deflate-bitstrøm der back-referansene kan nå over bitgrensene, og én Adler-32 over hele inputen. FPC-grenen har nå samme struktur som Delphi-grenen alltid har hatt. Den skriver også ut data etter hvert som de produseres, i stedet for å først kjede sammen hele det komprimerte resultatet i en AnsiString, så skriveren ikke lenger bygger en andre full kopi av de komprimerte dataene i minnet før den kopierer dem til målet
// FPC DeflateStream fra v3.539.24 (feilstier kuttet)
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); // samme tilstand, ingen medlemsbrudd
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 for hele inputen
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 fikk den symmetriske omskrivingen: én inflateInit2, en indre løkke som fortsetter å kalle inflate til gjeldende bit er konsumert og utdatabufferen ikke lenger er full, og et stopp på Z_STREAM_END. Det er én bieffekt verdt å vite om. Den gamle chunkede skriveren hardkodet nivå 6, mens den nye respekterer PLDeflateLevel, så et nivå satt gjennom TPDFlib.SetCompressionLevel(1..9) gjelder nå for store innebygde filer på FPC også. Det betyr noe hvis du allerede tuner komprimering for arkivutdata, som beskrevet i å redusere PDF-filstørrelse i Delphi
Hvordan verifiserer du at en Flate-strøm er ett enkelt zlib-medlem?
Inflater den med en helt vanlig zlib-dekoder og sjekk to ting når den returnerer Z_STREAM_END: den dekodede lengden er lik kildelengden, og avail_in er null. Input som ligger igjen etter slutmarkøren er signaturen på en sammenkjedet strøm. Fiksen ble verifisert slik: 200 KB testdata, som spenner over fire 64 KB-biter, gikk gjennom den nye DeflateStream og kom ut som en enkelt zlib-strøm på 534 byte, en standard zlib-dekoder hentet tilbake alle 200 000 bytene uten gjenværende input, og samme sjekk gikk bra på det krysskompilerte i386-målet for FPC. Rutinen nedenfor er FPC-versjonen av den sjekken, bygget direkte på paszlib slik at den ikke stoler på koden som testes
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;
// Ett medlem slutter nøyaktig ved den siste inputbyten
Result:= (Status = Z_STREAM_END) and (strm.avail_in = 0);
finally
inflateEnd(strm);
end;
end;
// Press 200 000 byte gjennom DeflateStream med standard 64 KB-bit
// og krev ett medlem som dekoder tilbake til full lengde
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å applikasjonsnivå er den nyttige påstanden den biblioteket ikke gjør for deg: sammenlign det som kommer ut av et vedlegg med /Params /Size registrert da det gikk inn. GetEmbeddedFileIntProperty med tag 5 returnerer den registrerte størrelsen, indeksene til innebygde filer er 1-baserte, og nyttelasten må være minst 1 MiB for å utløse strømmingsstien. Kjør samme test på hver kompilator du leverer med, siden den opprinnelige feilen gikk bra på Delphi og feilet bare 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 chunkede DeflateStream-stien 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); // /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;
Hva endrer ikke fiksen?
FPC-grenen beholder sine milde dekodingsregler, og den reparerer ikke filer som tidligere FPC-bygg allerede har skrevet. Når MaxOutput er nådd, avkorter FPC-InflateStream ved grensen og returnerer, mens Delphi-grenen reiser ERangeError, og FPC godtar fortsatt delvis dekodet resultat når inflate rapporterer en datafeil, fordi noen PDF-produsenter sender ut avkortete strømmer eller strømmer med ødelagt sjekksum. En PDF skrevet av et FPC-bygg før v3.539.24 inneholder fortsatt sammenkjedede medlemmer, og den korrigerte leseren stopper, som enhver annen leser, ved den første Z_STREAM_END. Ikke prøv å lege en slik fil ved å dekode og re-enkode strømmen inne i biblioteket, for det gjør bare 64 KB-avkortingen permanent. Legg heller ved vedlegget på nytt fra originalkilden sin. FPC-løkken slutter også fortsatt ved den første lesingen som returnerer mindre enn en full bit, som bare er et slutt-på-data-signal for strømmer som TFileStream og TMemoryStream, så en egendefinert TStream sendt til AddAssociatedFileFromStream er tryggest kopiert inn i en TMemoryStream først. Delphi-grenen, hjelperne DeflateStr og InflateStr, og alle strømmer under 1 MiB på den rene Flate-stien oppfører seg nøyaktig som før
Den korrigerte FPC-DeflateStream og InflateStream leveres i v3.539.24 av PDF Library for Delphi, som retter seg mot Delphi, C++Builder og Free Pascal fra ett kildekode-tre, og der skal nå et stort vedlegg komme tilbake fra et FPC-bygg byte for byte, på samme måte som det alltid har gjort fra Delphi