A v3.539.24 előtt a PDF Library for Delphi Free Pascal változata 64 KB-os darabokban tömörítette a nagy streameket, és minden darabnak saját zlib fejlécet és ellenőrzőösszeget adott, így egy 1 MiB-es melléklet tizenhat, végükön egymáshoz fűzött zlib taggé állt össze. Az ISO 32000-1 FlateDecode-je pontosan egyetlen zlib streamet vár, a szabványos dekóder pedig az első streamvége-jelnél áll meg, ami azt jelenti, hogy az első 65 536 bájt után minden csendben eltűnt. A javítás egyetlen zlib állapotot tart életben az összes darabon át, a DeflateStream-ben és az InflateStream-ben egyaránt
A hiba csak a unit FPC oldalán létezett, és csak a darabolt streaming útvonalon, pontosan ezért maradhatott rejtve: a Delphi ág mindig helyes volt, a string-alapú helperek mindig helyesek voltak, a kis teszt hasznos terhek pedig el sem jutottak a darabolt kódig. Rokonságban áll az öt FPC portolási hibával, amit a Delphi mindeddig elrejtett, azzal a különbséggel, hogy itt a Delphi build nem takart semmit. Az FPC ág egyszerűen egy téves mentális modell alapján íródott arról, mi is az a zlib stream
Miért csonkolják meg a többi PDF-olvasó a darabolt zlib streamet?
Mert az RFC 1950 által definiált zlib stream egyetlen konténer, nem konténerek sorozata, és a szabványos inflater az első Adler-32 trailerhez érést az adat végének veszi. A formátum egy kétbájtos fejléc, egy folyamatos RFC 1951 deflate bitsor, aminek utolsó blokkja hordozza a végső blokk flaget, illetve egy négybájtos Adler-32 ellenőrzőösszeg az összes tömörítetlen bájton. Az ISO 32000-1 §7.4.4-e pontosan így definiálja a /FlateDecode-et. Amikor az inflate eléri a trailert, Z_STREAM_END-et ad vissza, és a maradék bemenetet olvasatlanul hagyja az avail_in-ben. Az ő szemszögéből semmi baj nincs, ezért nem dob hibát, és a trailer utáni bájtokat egyszerűen figyelmen kívül hagyja. Az összefűzött tagok legitim ötlet a gzipben (az RFC 1952 megenged több tagot egy fájlban), valószínűleg innen jött az intuíció, de a zlibnek nincs ilyen szabálya, és a PDF soha nem kért ilyet
A régi FPC DeflateStream egyszerre 64 KB-ot olvasott a forrásból, és minden darabot átadott a ZFPCCompress-nek, egy helpernek, ami saját deflateInit2-t futtat, Z_FINISH-szel tömörít, majd meghívja a deflateEnd-et. Így minden darab komplett, érvényes, önlezárt zlib streamként jött ki, és a függvény ezeket egyetlen AnsiString-be fűzte össze, mielőtt kiírta volna. Az eredmény Flate adatnak nézett ki, helyes fejléce volt, és hiba nélkül dekódolódott, de csak az első 65 536 bájtot adta vissza. Az olvasó oldalon az InflateStream a tükörképi hibát hordozta: 64 KB tömörített bemenetenként egyszer hívta a ZFPCInflate-et, és minden hívás friss inflateInit2-vel indult. A második darab egy deflate bitsor közepén kezdődik, zlib fejléc nélkül, így egy új inflater elutasítja, és egy teljesen normális, egytagú stream bármely más gyártótól csak addig dekódolódott, ameddig az első 64 KB tömörített bájtja elvitte
// FPC DeflateStream a v3.539.24 előtt (egyszerűsítve):
// a ZFPCCompress deflateInit2 / deflate(Z_FINISH) / deflateEnd sorozatot futtat,
// így minden 64 KB-os darab külön zlib taggé válik
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));
Mely PDF Library for Delphi hívások érték el a darabolt útvonalat?
FPC-n minden 1 MiB-es vagy nagyobb beágyazott fájl rosszul íródott, és minden, a streaming API-n keresztül kinyert Flate stream rosszul olvasódott, amint a tömörített mérete átlépett egy darabot. A TPDFStream.ReadFromStream dönti el, hogyan kódolja a bejövő adatot. Ha a Deflate true és a Stream.Size >= 1048576, akkor a DeflateStream-en keresztül streameli; ez alatt a küszöb alatt az egész forrást memóriába olvassa, és a DeflateStr-t hívja, azt az egyszeri helpert, amit a hiba soha nem érintett. Az ASCII85-plusz-Flate szűrőlánc kihagyja a méretvizsgálatot, és mindig a DeflateStream-en megy át, így ezen az útvonalon minden 64 KB-nál nagyobb hasznos teher már több tagra szakadt szét. A nyilvános belépési pontok, amik bekapcsolt tömörítéssel etetik a ReadFromStream-et, a beágyazottfájl-írók:
TPDFlib.EmbedFileésTPDFlib.AddEmbeddedFile, amelyek fájlt olvasnak a lemezről egy/EmbeddedFilestreambeTPDFlib.AddAssociatedFileFromStreamésTPDFlib.AddAssociatedFileFromFile, a PDF/A-3 társítottfájl-írók, amiket e-számla XML és más forrásadatokhoz használnak- Az olvasó oldalon a
TPDFlib.GetEmbeddedFileContentToStreamés aGetEmbeddedFileContentToFile, amelyek aTPDFStream.WriteDecodedToStream-en, majd onnan azInflateStream-en keresztül dekódolnak
A hiba minden rétegen csendes volt. Az író tárolja a /Params /Size-t és az eredeti fájlból számolt MD5 /CheckSum-t, így a mellékletszótár a teljes méretet hirdette, miközben a stream tizenhat tagot tartott. Ugyanennek a librarynek egy Delphi buildje olvasva azt a fájlt szépen megállt az első Z_STREAM_END-nél, és pontosan 65 536 bájtot adott vissza. A GetEmbeddedFileContentToStream 1-et adott vissza, mert azt jelzi, hogy a dekódolás dobott-e hibát, nem azt, hogy a kimenet egyezik-e a /Size-szal. Aki már üldözött nagy dokumentumos problémát a gigabájtos PDF-ek összefésülése és szétvágása során, az ismeri ezt a mintát: a fájl megnyílik, az oldalszám stimmel, és a kár csak akkor látszik, amikor valaki megnyitja a mellékletet
Egy deflate állapot az összes darabon át
A javított DeflateStream a PDFlibZLib.pas-ban egyetlen paszlib.TZStream-et inicializál, minden darabot Z_NO_FLUSH-szal etet a deflate-nek, és csak a végén csapolja meg a tömörítőt Z_FINISH-szel, amíg az Z_STREAM_END-et nem ad vissza. Ez pontosan egy fejlécet, egy deflate bitsort eredményez, aminek visszahivatkozásai átnyúlhatnak a darabhatárokon, és egy Adler-32-t a teljes bemenetre. Az FPC ág mostantól ugyanazt a struktúrát hordozza, amit a Delphi ág mindig is. Ráadásul a kimenetet akkor írja ki, ahogy keletkezik, ahelyett hogy előbb a teljes tömörített eredményt egy AnsiString-be fűzné össze, így az író már nem épít második teljes másolatot a tömörített adatból a memóriában, mielőtt a célba másolná
// FPC DeflateStream a v3.539.24-től (hibautak lerövidítve)
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); // ugyanaz az állapot, nincs taghatár
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 // egy trailer a teljes bemenetre
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;
Az InflateStream a szimmetrikus átírást kapta: egy inflateInit2, egy belső ciklus, ami addig hívja az inflate-et, amíg az aktuális darab el nem fogy és a kimeneti buffer már nem teli, illetve egy megállás Z_STREAM_END-en. Van egy mellékhatás, amit érdemes ismerni. A régi darabolt író beégetve hordozta a 6-os szintet, az új pedig tiszteletben tartja a PLDeflateLevel-t, így a TPDFlib.SetCompressionLevel(1..9)-ön keresztül beállított szint mostantól az FPC-n is vonatkozik a nagy beágyazott fájlokra. Ez azért számít, ha már most is hangolod a tömörítést archív kimenethez, ahogy azt a PDF-fájlméret csökkentése Delphiben írja
Hogyan ellenőrzöd, hogy egy Flate stream egyetlen zlib tag?
Tömörítsd ki egy sima zlib dekóderrel, és amikor Z_STREAM_END-et ad vissza, ellenőrizz két dolgot: a dekódolt hossz egyenlő-e a forrás hosszával, és az avail_in nulla-e. A végjel után maradó bemenet az összefűzött stream kézjegye. A javítás így bizonyított: 200 KB tesztadat, ami négy 64 KB-os darabra terül szét, átmegy az új DeflateStream-en, és 534 bájtos, egytagú zlib streamként jön ki, egy stock zlib dekóder visszaadja mind a 200 000 bájtot maradék bemenet nélkül, és ugyanez az ellenőrzés átmegy az i386 cross-compiled FPC célon is. Az alábbi rutin annak az ellenőrzésnek az FPC változata, közvetlenül a paszlib-re építve, hogy ne bízzon a tesztelt kódban
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;
// Egy tag pontosan az utolsó bemeneti bájtnál ér véget
Result:= (Status = Z_STREAM_END) and (strm.avail_in = 0);
finally
inflateEnd(strm);
end;
end;
// Nyomj át 200 000 bájtot a DeflateStream-en az alapértelmezett 64 KB-os darabbal,
// és követelj meg egy tagot, ami visszafejtve a teljes hosszt adja
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');
Alkalmazási szinten a hasznos kijelentés az, amit a library nem csinál meg helyetted: hasonlítsd össze, ami egy mellékletből kijön, azzal a /Params /Size-szal, amit a bevitelekor rögzítettek. A GetEmbeddedFileIntProperty 5-ös taggel azt a rögzített méretet adja vissza, a beágyazottfájl-indexek 1-től indulnak, és a hasznos tehernek legalább 1 MiB-nek kell lennie, hogy a streaming útvonalra kerüljön. Futtasd ugyanezt a tesztet minden fordítón, amivel szállítod, hiszen az eredeti hiba a Delphi ágon nem jelentkezett, csak az FPC-n
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 vagy több a darabolt DeflateStream útvonalat veszi a ReadFromStream-ben
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 nem változtat meg a javítás?
Az FPC ág megtartja az engedékeny dekódolási szabályait, és nem javítja meg azokat a fájlokat, amiket korábbi FPC build-ek már megírtak. Amikor a MaxOutput határát éri el, az FPC InflateStream a határnál csonkol és visszatér, míg a Delphi ág ERangeError-t dob, és az FPC továbbra is elfogad részben dekódolt kimenetet, amikor az inflate adathibát jelent, mert egyes PDF-gyártók csonkolt vagy ellenőrzőösszeg-sértett streameket bocsátanak ki. Egy v3.539.24 előtti FPC build által írt PDF továbbra is összefűzött tagokat tartalmaz, és a javított olvasó, akárcsak minden más olvasó, az első Z_STREAM_END-nél áll meg. Ne próbáld ilyen fájlt a stream libraryn belüli dekódolásával és újratömörítésével meggyógyítani, mert az csak véglegesíti a 64 KB-os csonkolást. Ehelyett ágyazd be újra a mellékletet az eredeti forrásából. Az FPC ciklus továbbra is az első, teljes darabnál kevesebbet adó olvasásnál ér véget, ami csak a TFileStream és a TMemoryStream fajta streameknél adatvége-jel, így egy egyedi TStream, amit a AddAssociatedFileFromStream-nak adsz át, a legbiztonságosabban úgy viselkedik, ha előbb egy TMemoryStream-be másolod. A Delphi ág, a DeflateStr és InflateStr helperek, és minden 1 MiB-nál kisebb stream a sima Flate útvonalon pontosan úgy viselkedik, mint korábban
A javított FPC DeflateStream és InflateStream a PDF Library for Delphi v3.539.24-ében érkezik, amely egyetlen forrásfáról támogatja a Delphit, a C++Buildert és a Free Pascalt, és amelyben egy nagy melléklet mostantól bájtról bájtra, ugyanúgy tér vissza egy FPC buildből, ahogy mindig is visszatért a Delphiből