Techninis straipsnis

Chunked zlib FPC: kodėl FlateDecode reikia vieno srauto

Iki v3.539.24 Free Pascal versija PDF Library for Delphi didelius srautus spaudė 64 KB gabalais ir kiekvienam gabalui dėdavo savo zlib antraštę bei kontrolinę sumą, tad 1 MiB priedas virsdavo šešiolika galo su galu suklijuotų zlib narių. ISO 32000-1 FlateDecode tikisi lygiai vieno zlib srauto, o standartinis dekoderis sustoja prie pirmosios srauto pabaigos žymos – tai reiškia, kad visa, kas buvo po pirmųjų 65 536 baitų, tyliai dingo. Pataisa DeflateStream ir InflateStream viduje palaiko vieną zlib būseną per visus gabalus

Klaida gyveno tik unito FPC pusėje ir tik gabalų srautinio perdavimo kelyje – būtent todėl ji ir išgyveno: Delphi šaka visuomet buvo teisinga, eilučių pagalbinės funkcijos visuomet buvo teisingos, o maži testiniai duomenų paketai iki gabalų kodo apskritai nepasiekdavo. Ji yra artima giminaitė defektams iš straipsnio apie penkias FPC perkėlimo klaidas, kurias slapstė Delphi, tik čia Delphi versija nieko nedengė. FPC šaka buvo tiesiog parašyta remiantis klaidingu įsivaizdavimu, kas iš tikrųjų yra zlib srautas

Kodėl kitos PDF peržiūros programos apkarpo gabalais rašytą zlib srautą?

Nes RFC 1950 apibrėžtas zlib srautas yra vienas konteineris, o ne jų seka, ir standartą geriantis inflater pirmąjį Adler-32 užraktą laiko duomenų pabaiga. Formatas yra dviejų baitų antraštė, viena nenutrūkstama RFC 1951 deflate bitų srautas, kurio paskutinis blokas neša final-block vėliavėlę, ir keturių baitų Adler-32 kontrolinė suma visiems nesuspaustiems baitams. ISO 32000-1 §7.4.4 /FlateDecode apibrėžia būtent tais žodžiais. Kai inflate pasiekia užraktą, jis grąžina Z_STREAM_END, o likusį neiskaitytą įvedimą palieka avail_in. Jo požiūriu nieko bloga neįvyko, todėl klaidos jis nekelia, o baitus po užrakto tiesiog ignoruoja. Sujungti nariai gzip formatuote yra teisėta idėja (RFC 1952 leidžia kelis narius viename faile) – greičiausiai iš ten ir atėjo ta intuicija, bet zlib tokios taisyklės neturi, o PDF jos niekada neprašė

RFC 1950 zlib nario anatomija PDFlibPas FlateDecode srautuose: dviejų baitų antraštė, viena nenutrūkstama deflate bitų srautas, kurio paskutinis blokas neša final-block vėliavėlę, ir keturių baitų Adler-32 užraktas, kuriame inflate grąžina Z_STREAM_END, palieka likusį įvedimą neiskaitytą avail_in ir nekelia jokios klaidos
Standartą geriantis inflater pirmąjį Adler-32 užraktą laiko duomenų pabaiga, tad nario riba yra kieta riba, o viskas, ką rašytojas prie jos priklijavo, yra negyvas svoris, kurio neatsakins joks skaitytuvas

Sena FPC DeflateStream skaitė šaltinį po 64 KB ir kiekvieną gabalą perdavinėjo ZFPCCompress – pagalbinei funkcijai, kuri pati paleidžia deflateInit2, spaudžia su Z_FINISH ir iškviečia deflateEnd. Kiekvienas gabalas taip išeidavo kaip pilnas, teisingas, pats save uždarantis zlib srautas, o funkcija juos sujungdavo į vieną AnsiString prieš rašydama į išvedimą. Rezultatas atrodė kaip Flate duomenys, turėjo teisingą antraštę ir dekodavosi be klaidų, bet dekoduodavosi tik iki pirmųjų 65 536 baitų. Skaitymo pusėje InflateStream turėjo veidrodišką defektą: jis kviesdavo ZFPCInflate po kartą kiekvieniems 64 KB suspaustų duomenų, ir kiekvienas iškvietimas startuodavo su nauja inflateInit2. Antras gabalas prasideda deflate bitų srauto viduryje be zlib antraštės, tad naujas inflater jį atmeta, ir visiškai įprastas vieno nario srautas iš bet kurio kito generatoriaus būdavo dekoduotas tik tiek, kiek nešė pirmieji jo 64 KB suspaustų baitų

Gabalų rašytojo defektas PDFlibPas FPC DeflateStream: kiekvienas 64 KB gabalas eina pro ZFPCCompress kaip pilnas zlib narys, tad 1 MiB priedas turi šešiolika suklijuotų narių, skaitytuvas sustoja prie pirmojo užrakto po 65 536 baitų, o GetEmbeddedFileContentToStream vis tiek praneša sėkmę
Žala liko nematoma, nes kiekvienas sluoksnis suveikė sėkmingai: priedo žodynas reklamavo visą /Params /Size, dekodavimas nekėlė klaidos, o apkarpytimą pastebėdavo tik tas, kas atverdavo priedą
// FPC DeflateStream iki v3.539.24 (supaprastinta):
// ZFPCCompress atlieka deflateInit2 / deflate(Z_FINISH) / deflateEnd,
// tad kiekvienas 64 KB gabalas tampa atskiru zlib nariu
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));

Kurie PDF Library for Delphi iškvietimai patekdavo į gabalų kelią?

FPC aplinkoje bet koks įmontuotas failas nuo 1 MiB ir daugiau būdavo rašomas neteisingai, o bet koks Flate srautas, ištrauktas pro srautinę API, būdavo skaitomas neteisingai, tik jo suspaustas dydis peržengdavo vieną gabalą. TPDFStream.ReadFromStream nusprendžia, kaip koduoti gaunamus duomenis. Kai Deflate yra true ir Stream.Size >= 1048576, duomenys keliauja pro DeflateStream; žemiau tos ribos visas šaltinis skaitomas į atmintį ir kviečiamas DeflateStr – vienkartė pagalbinė funkcija, kurios šis defektas niekada nepalietė. ASCII85 plus Flate filtrų grandinė dydžio testo nepaiso ir visuomet eina pro DeflateStream, tad tame kelyje bet kokie duomenys, didesni nei 64 KB, jau būdavo suskaidyti į kelis narius. Viešieji įėjimo taškai, kurie maitina ReadFromStream įjungtu suspaudimu, yra įmontuotų failų rašytojai:

  • TPDFlib.EmbedFile ir TPDFlib.AddEmbeddedFile, kurie iš disko skaito failą į /EmbeddedFile srautą
  • TPDFlib.AddAssociatedFileFromStream ir TPDFlib.AddAssociatedFileFromFile – PDF/A-3 susietųjų failų rašytojai, naudojami el. sąskaitų XML ir kitiems šaltinio duomenims
  • Skaitymo pusėje – TPDFlib.GetEmbeddedFileContentToStream ir GetEmbeddedFileContentToFile, kurie dekoduoja pro TPDFStream.WriteDecodedToStream, o iš ten pro InflateStream

Gedimas buvo tylus kiekviename sluoksnyje. Rašytojas saugo /Params /Size ir MD5 /CheckSum, apskaičiuotą iš originalaus failo, tad priedo žodynas reklamavo pilną dydį, kol sraute gulėjo šešiolika narių. Tos pačios bibliotekos Delphi versija, skaičiusi tą failą, švariai sustodavo prie pirmojo Z_STREAM_END ir grąžindavo lygiai 65 536 baitų. GetEmbeddedFileContentToStream grąžindavo 1, nes jis praneša, ar dekodavimas kėlė klaidą, o ne ar išvedimas sutampa su /Size. Kas benarštėtų didelio dokumento problemą pro gigabaitinių PDF sujungimą ir skaidymą, tas pažįsta šį raštą: failas atsiverčia, puslapių skaičius teisingas, o žala išryškėja tik tada, kai kas nors atveria priedą

Viena deflate būsena per visus gabalus

Pataisytas DeflateStream faile PDFlibZLib.pas inicializuoja vieną paszlib.TZStream, kiekvieną gabalą duoda deflate su Z_NO_FLUSH, o tik pačioje pabaigoje ištuština kompresorių su Z_FINISH, kol šis grąžina Z_STREAM_END. Taip gaunama lygiai viena antraštė, vienas deflate bitų srautas, kurio atgalinės nuorodos gali persitiesti per gabalų ribas, ir vienas Adler-32 visam įvedimui. FPC šaka dabar turi tą pačią struktūrą, kurią Delphi šaka turėjo visada. Be to, išvedimas rašomas jau jį gaminant, o ne iš anksto sujungiant visą suspaustą rezultatą į AnsiString, tad rašytojas daugiau nebekuria antros pilnos suspaustų duomenų kopijos atmintyje prieš nukopijuodamas ją į taikinį

Pataisytas PDFlibPas DeflateStream: vienas paszlib.TZStream inicializuojamas kartą, kiekvienas 64 KB gabalas duodamas su Z_NO_FLUSH, o pabaigoje seka Z_FINISH ištuštinimas; gaunama lygiai viena antraštė, vienas nenutrūkstamas deflate bitų srautas, kurio atgalinės nuorodos kerta gabalų ribas, ir vienas Adler-32 visam įvedimui
Viena būsena pakeičia ir tai, kaip rašomas išvedimas: suspausti baitai išeina, kai tik prisipildo buferis, o ne kaupiasi antroje pilnoje kopijoje, ir PLDeflateLevel dabar pasiekia didelius įmontuotus failus ir FPC aplinkoje
// FPC DeflateStream nuo v3.539.24 (klaidų atšakos iškirptos)
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);   // ta pati būsena, jokio nario lūžio
        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                                    // vienas užraktas visam įvedimui
    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 sulaukė simetriško perrašymo: vienas inflateInit2, vidinė kilpa, kuri nenustoja kvietusi inflate, kol dabartinis gabalas išvartojamas ir išvedimo buferis nebeprisipildo, ir sustojimas ties Z_STREAM_END. Yra vienas šalutinis efektas, vertas žinoti. Senas gabalų rašytojas lygį 6 turėjo įkoduotą, o naujasis gerbia PLDeflateLevel, tad lygis, nustatytas pro TPDFlib.SetCompressionLevel(1..9), dabar galioja ir dideliems įmontuotiems failams FPC aplinkoje. Tai svarbu, jei suspaudimą jau derinate archyviniams išvedimams, kaip aprašyta straipsnyje apie PDF failo dydžio mažinimą Delphi aplinkoje

Kaip patikrinti, ar Flate srautas yra vienas zlib narys?

Išpūskite jį paprastu zlib dekoderiumi ir kai jis grąžina Z_STREAM_END, pasitikrinkite du dalykus: dekoduotas ilgis lygus šaltinio ilgiui, o avail_in lygus nuliui. Likutis įvedimo po pabaigos žymos yra sujungto srauto pirštų atspaudas. Pataisa buvo patikrinta būtent taip: 200 KB testinių duomenų, dengiančių keturis 64 KB gabalus, praėjo pro naująjį DeflateStream ir išėjo kaip 534 baitų vienas zlib srautas, standartinis zlib dekoderis atkūrė visus 200 000 baitų be likusio įvedimo, o tas pats testas praeito ir i386 cross-compile FPC taikinyje. Žemiau esanti procedūra yra tos patikros FPC versija, paremta tiesiogiai paszlib, kad netektų pasitikėti patikrinamu kodu

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;
    // Vienas naris baigiasi lygiai paskutiniame įvedimo baite
    Result:= (Status = Z_STREAM_END) and (strm.avail_in = 0);
  finally
    inflateEnd(strm);
  end;
end;

// Perkiškite 200 000 baitų pro DeflateStream su numatytuoju 64 KB gabalu
// ir reikalaukite vieno nario, kuris dekoduojasi atgal į pilną ilgį
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');

Programos lygiu naudingiausia patikra yra ta, kurios biblioteka už jus nepadaro: palyginkite, kas išeina iš priedo, su /Params /Size, įrašytu jį įdedant. GetEmbeddedFileIntProperty su žyme 5 grąžina tą įrašytą dydį, įmontuotų failų indeksai skaičiuojami nuo 1, o duomenų paketas turi būti bent 1 MiB, kad pasiektų srautinį kelią. Tą patį testą paleiskite su kiekvienu kompiliatoriumi, su kuriuo pristatote produktą, nes originalus defektas Delphi aplinkoje praėjo sėkmingai, o žlugdavo tik 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 ir daugiau eina gabalų DeflateStream keliu ReadFromStream viduje
    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;

Ką pataisa nekeičia?

FPC šaka lieka prie savo atlaisvintų dekodavimo taisyklių ir negydo failų, kuriuos jau buvo parašę ankstesni FPC variantai. Pasiekus MaxOutput, FPC InflateStream nukerpa ties riba ir grįžta, kol Delphi šaka kelia ERangeError, o FPC vis tiek priima iš dalies dekoduotą išvedimą, kai inflate praneša duomenų klaidą, nes kai kurie PDF generatoriai išveda apkerptus arba su sugadinta kontroline suma srautus. PDF, parašytas FPC versijos iki v3.539.24, vis dar turi sujungtus narius, ir pataisytas skaitytuvas, kaip ir bet kuris kitas, sustoja prie pirmojo Z_STREAM_END. Negydykite tokio failo dekoduodami ir pakartotinai koduodami srautą bibliotekos viduje – tai tik įamžins 64 KB apkarpytimą. Geriau vėl įmontuokite priedą iš jo pirminio šaltinio. FPC kilpa taip pat vis dar baigiasi pirmuoju skaitymu, grąžinusiu mažiau nei pilną gabalą, o tai duomenų pabaigos signalas tik srautams, tokiems kaip TFileStream ir TMemoryStream, tad nuosavą TStream, perduodamą AddAssociatedFileFromStream, saugiausia pirmiausia nukopijuoti į TMemoryStream. Delphi šaka, DeflateStr ir InflateStr pagalbinės funkcijos ir kiekvienas mažesnis nei 1 MiB srautas paprastame Flate kelyje elgiasi lygiai taip, kaip anksčiau

Pataisytos FPC DeflateStream ir InflateStream atkeliauja su PDF Library for Delphi v3.539.24, kuri iš vieno šaltinio medžio palaiko Delphi, C++Builder ir Free Pascal, ir kurioje didelis priedas iš FPC versijos dabar turėtų grįžti baitas po baito – taip, kaip visada grįždavo iš Delphi