پیش از v3.539.24، بیلد Free Pascal از PDF Library for Delphi استریمهای بزرگ را در chunkهای 64 KB فشرده میکرد و به هر chunk هدر و checksum مخصوص خودش را میداد، پس یک پیوست 1 MiBایی میشد شانزده عضو zlib که تهبهته به هم چسبیده بودند. FlateDecode در ISO 32000-1 دقیقاً یک zlib stream انتظار دارد و یک decoder استاندارد روی اولین نشانگر end-of-stream میایستد، یعنی هرچه بعد از 65,536 بایت اول بود بیسروصدا گم میشد. fix یک zlib state واحد را در هر دو DeflateStream و InflateStream سراسر همهٔ chunkها زنده نگه میدارد
این باگ فقط در سمت FPC این unit و فقط روی مسیر استریم chunkشده وجود داشت، و دقیقاً به همین دلیل زنده مانده بود: شاخهٔ Delphi همیشه درست بود، هلپرهای رشتهمحور همیشه درست بودند و payloadهای تست کوچک هرگز اصلاً به کد chunkشده نمیرسیدند. خویشاوند نزدیک نقصهای پنج باگ پورت FPC که Delphi پنهانشان میکرد است، با این تفاوت که اینجا بیلد Delphi سرپوش چیزی نبود. شاخهٔ FPC صرفاً با یک مدل ذهنی غلط از اینکه zlib stream چیست نوشته شده بود
چرا readerهای PDF دیگر یک zlib stream chunkشده را نصفه میخوانند؟
چون zlib stream طبق تعریف RFC 1950 یک container است، نه دنبالهای از آنها، و یک inflater قانونمند اولین trailer آدلر-32 را پایان داده میداند. قالب یک هدر دو بایتی است، یک بیتاستریم پیوستهٔ deflate طبق RFC 1951 که بلوک آخرش فلگ final-block را حمل میکند، و یک checksum چهاربایتی Adler-32 روی همهٔ بایتهای غیرفشرده. ISO 32000-1 §7.4.4 /FlateDecode را دقیقاً با همین اصطلاحات تعریف میکند. وقتی inflate به trailer میرسد Z_STREAM_END برمیگرداند و باقیماندهٔ ورودی را نخوانده در avail_in رها میکند. از دید او هیچ چیز غلط نیست، پس خطایی raise نمیشود و بایتهای بعد از trailer صرفاً نادیده گرفته میشوند. اعضای چسباندهشده در gzip ایدهٔ مشروعیاند (RFC 1952 چند عضو در یک فایل را مجاز میداند) و احتمالاً شهود از همانجا آمده، اما zlib چنین قاعدهای ندارد و PDF هم هرگز خواستارش نبوده
DeflateStream قدیمی FPC منبع را هر بار 64 KB میخواند و هر chunk را به ZFPCCompress میداد، هلپری که deflateInit2 خودش را اجرا میکند، با Z_FINISH فشرده میکند و deflateEnd را صدا میزند. پس هر chunk از آب درمیآمد یک zlib stream کامل، معتبر و خودخاتمه، و تابع آنها را پیش از نوشتن در یک AnsiString به هم میچسباند. نتیجه شبیه دادهٔ Flate بود، هدر درستی داشت و بیخطا decode میشد، اما فقط به 65,536 بایت اول decode میشد. در سمت خواندن، InflateStream نقص آینهای داشت: بهازای هر 64 KB ورودی فشرده یک بار ZFPCInflate صدا زده میشد و هر فراخوانی inflateInit2 تازهای شروع میکرد. chunk دوم از میانهٔ یک بیتاستریم deflate و بدون هدر zlib شروع میشود، پس inflater تازه آن را رد میکند، و یک stream تکعضوی کاملاً معمولی از هر تولیدکنندهٔ دیگری فقط تا جایی decode میشد که 64 KB اول بایتهای فشردهاش پیش میبرد
// DeflateStream سمت FPC پیش از v3.539.24 (سادهشده):
// ZFPCCompress کار deflateInit2 / deflate(Z_FINISH) / deflateEnd را میکند،
// پس هر chunk 64 KB یک عضو zlib جدا میشود
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));
کدام فراخوانیهای PDF Library for Delphi به مسیر chunkشده میرسند؟
روی FPC هر فایل embedded یک MiB یا بیشتر غلط نوشته میشد و هر Flate stream که از طریق API استریمینگ استخراج میشد، بهمحض عبور اندازهٔ فشردهاش از یک chunk، غلط خوانده میشد. TPDFStream.ReadFromStream تصمیم میگیرد دادهٔ ورودی چطور encode شود. وقتی Deflate true باشد و Stream.Size >= 1048576، داده از DeflateStream عبور میکند؛ زیر آن آستانه، کل منبع در حافظه خوانده و DeflateStr صدا زده میشود، همان هلپر تکمرحلهای که هرگز آسیبی ندید. زنجیرهٔ فیلتر ASCII85 بهعلاوهٔ Flate آزمون اندازه را رد میکند و همیشه از DeflateStream میگذرد، پس در آن مسیر هر payload بزرگتر از 64 KB از قبل به چند عضو تقسیم میشد. نقطههای ورود عمومی که با فشردهسازی روشن به ReadFromStream غذا میدهند writerهای فایل embedded هستند:
TPDFlib.EmbedFileوTPDFlib.AddEmbeddedFileکه یک فایل را از دیسک خوانده و در یک stream/EmbeddedFileمینویسندTPDFlib.AddAssociatedFileFromStreamوTPDFlib.AddAssociatedFileFromFile، writerهای فایل مرتبط PDF/A-3 که برای XML فاکتور الکترونیکی و دیگر دادههای مبدأ به کار میروند- در سمت خواندن،
TPDFlib.GetEmbeddedFileContentToStreamوGetEmbeddedFileContentToFileکه از طریقTPDFStream.WriteDecodedToStreamو از آنجا از طریقInflateStreamdecode میکنند
شکست در هر لایه بیصدا بود. writer /Params /Size و یک /CheckSum از جنس MD5 را که از فایل اصلی محاسبه شده ذخیره میکند، پس dictionary پیوست اندازهٔ کامل را اعلان میکرد در حالی که stream شانزده عضو در خود داشت. بیلد Delphi همان کتابخانه که چنین فایلی را میخواند تمیز روی اولین Z_STREAM_END میایستاد و دقیقاً 65,536 بایت برمیگرداند. GetEmbeddedFileContentToStream مقدار 1 برمیگرداند، چون گزارش میکند decode خطا داده یا نه، نه اینکه خروجی با /Size بخواند یا نه. هر کسی که یک مسئلهٔ سند بزرگ را تا ادغام و تفکیک PDFهای گیگابایتی دنبال کرده این الگو را میشناسد: فایل باز میشود، تعداد صفحهها درست است و آسیت فقط وقتی خودش را نشان میدهد که یکی پیوست را باز کند
یک deflate state سراسر همهٔ chunkها
DeflateStream اصلاحشده در PDFlibZLib.pas یک paszlib.TZStream واحد را مقداردهی اولیه میکند، هر chunk را با Z_NO_FLUSH به deflate میدهد و فقط در پایان کمپرسور را با Z_FINISH تخلیه میکند تا Z_STREAM_END برگرداند. نتیجه دقیقاً یک هدر است، یک بیتاستریم deflate که back-referenceهایش میتوانند از مرز chunkها بگذرند، و یک Adler-32 روی کل ورودی. شاخهٔ FPC حالا همان ساختاری را دارد که شاخهٔ Delphi همیشه داشت. خروجی هم همانطور که تولید میشود نوشته میشود، بهجای آنکه تمام نتیجهٔ فشرده اول در یک AnsiString جمع شود، پس writer دیگر پیش از کپی کردن به مقصد، یک کپی کامل دوم از دادهٔ فشرده در حافظه نمیسازد
// DeflateStream سمت FPC از v3.539.24 (مسیرهای خطا حذف شده)
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); // همان state، بدون شکستن عضو
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 // یک trailer برای کل ورودی
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 بازنویسی متقارن را گرفت: یک inflateInit2، حلقهٔ داخلی که تا مصرف شدن chunk جاری و خالی شدن بافر خروجی به صدا زدن inflate ادامه میدهد، و یک توقف روی Z_STREAM_END. یک عارضهٔ جانبی هم هست که بدانش بد نیست. writer چانکشدهٔ قدیمی سطح 6 را هاردکد کرده بود، در حالی که نسخهٔ جدید به PLDeflateLevel احترام میگذارد، پس سطحی که از طریق TPDFlib.SetCompressionLevel(1..9) ست شده حالا روی فایلهای embedded بزرگ در FPC هم اعمال میشود. اگر برای خروجی آرشیوی فشردهسازی را تنظیم کردهای — همانطور که در کاهش حجم فایل PDF در Delphi آمده — این برایت مهم است
چطور بررسی کنیم یک Flate stream تکعضویِ zlib است؟
با یک decoder zlib ساده inflateاش کن و وقتی Z_STREAM_END برگرداند دو چیز را بررسی کن: طول decodeشده با طول منبع برابر باشد و avail_in صفر باشد. ورودی باقیمانده بعد از نشانگر پایان، امضای یک stream چسباندهشده است. fix همینطور راستیآزمایی شد: 200 KB دادهٔ تست که چهار chunk 64 KB را پوشش میدهد از DeflateStream جدید گذشت و یک zlib stream تکعضوی 534 بایتی از آب درآمد، یک decoder zlib معمولی هر 200,000 بایت را بدون ورودی باقیمانده بازیابی کرد و همین بررسی روی target کراسکامپایلشدهٔ i386 در FPC هم قبول شد. روتیین زیر نسخهٔ FPC همان بررسی است که مستقیماً روی paszlib ساخته شده تا به کدی که زیر آزمون است اعتماد نکند
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;
// یک عضو دقیقاً روی آخرین بایت ورودی تمام میشود
Result:= (Status = Z_STREAM_END) and (strm.avail_in = 0);
finally
inflateEnd(strm);
end;
end;
// 200,000 بایت را با chunk پیشفرض 64 KB از DeflateStream عبور بده
// و یک عضو بخواه که به طول کامل decode شود
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');
در سطح اپلیکیشن، ادعای مفید همان است که کتابخانه بهجایت نمیسازد: خروجی پیوست را با /Params /Size ثبتشده هنگام ورودش مقایسه کن. GetEmbeddedFileIntProperty با tag 5 همان اندازهٔ ثبتشده را برمیگرداند، شاخصهای فایل embedded یکمبنایند و payload باید دستکم 1 MiB باشد تا مسیر استریمینگ را بچرخانی. همان تست را روی هر کامپایلری که با آن ship میکنی اجرا کن، چون نقص اصلی روی Delphi قبول و فقط روی 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');
// یک MiB یا بیشتر مسیر DeflateStream چانکشده را در 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;
fix چه چیزهایی را عوض نمیکند؟
شاخهٔ FPC قواعد آسانگیر decode کردنش را نگه میدارد و فایلهایی را که بیلدهای قبلی FPC نوشتهاند ترمیم نمیکند. وقتی به MaxOutput برسیم، InflateStream سمت FPC روی همان حد بریده و برمیگردد، در حالی که شاخهٔ Delphi ERangeError میدهد؛ و FPC همچنان وقتی inflate خطای داده گزارش میکند خروجی نیمهdecodeشده را میپذیرد، چون بعضی تولیدکنندههای PDF استریمهای بریده یا checksumشکسته صادر میکنند. PDFی که یک بیلد FPC پیش از v3.539.24 نوشته هنوز اعضای چسبیده در خود دارد و reader اصلاحشده، مثل هر reader دیگری، روی اولین Z_STREAM_END میایستد. سعی نکن چنین فایلی را با decode و encode دوبارهٔ stream داخل کتابخانه مداوا کنی، چون فقط بریدگی 64 KB را دائمی میکند. بهجایش پیوست را از منبع اصلیاش دوباره embed کن. حلقهٔ FPC همچنان روی اولین خواندنی که کمتر از یک chunk کامل برگرداند تمام میشود، که فقط برای استریمهایی مثل TFileStream و TMemoryStream نشانهٔ پایان داده است، پس یک TStream سفارشی که به AddAssociatedFileFromStream میدهی بهتر است اول در یک TMemoryStream کپی شود. شاخهٔ Delphi، هلپرهای DeflateStr و InflateStr و هر stream کوچکتر از 1 MiB در مسیر Flate ساده دقیقاً مثل قبل رفتار میکنند
DeflateStream و InflateStream اصلاحشدهٔ FPC در v3.539.24 از PDF Library for Delphi عرضه میشوند، کتابخانهای که Delphi و C++Builder و Free Pascal را از یک source tree هدف میگیرد، و جایی که حالا باید یک پیوست بزرگ از بیلد FPC هم بایتبهبایت برگردد، همانطور که همیشه از Delphi برمیگشت