مقاله فنی

HotPDF روی Free Pascal: محدودیت‌های Deflate، AES و کدک

HotPDF زیر Free Pascal 3.2.2 با Lazarus کامپایل و اجرا می‌شود، و خلاصه صادقانه آن پورت دو جمله است. ساخت، بارگذاری، ذخیره، فشرده‌سازی، بازکردن فشرده، رمزگذاری و رمزگشایی سند همگی روی بک‌اندهای فقط-پاسکالی کار می‌کنند، پس یک برنامه Lazarus می‌تواند PDF واقعی بدون هیچ وابستگی C تولید و مصرف کند. کدک‌های تصویر بومی اختیاری این‌طور نیستند، چون آبجکت‌های از پیش ساخته Win64 از گونه‌ای COFF استفاده می‌کنند که هیچ‌کدام از دو لینکر Free Pascal نمی‌تواند مصرف کند، پس روی آن toolchain نقاط ورود به stubهایی می‌روند که fail closed می‌شوند

نقشه قابلیت HotPDF روی Free Pascal: بک‌اندهای پاسکالی کارکنان deflate و AES و سند در کنار stubهای کدک تصویر که fail closed می‌شوند
ویژگی‌های سند، فشرده‌سازی و رمزگذاری روی بک‌اندهای فقط-پاسکالی اجرا می‌شوند، در حالی که کدک‌های تصویر بومی به stubهایی می‌روند که fail closed می‌شوند

رسیدن از «کامپایل می‌شود» به «کار می‌کند» مجموعه مشخصی از fixها برد، و هر یک از آن‌ها تله‌ای است که هر codebase دلفی دیگری را که به Free Pascal کوچ می‌کند پیدا خواهد کرد. ارزش نوشتن دارند به ترتیبی که دردشان می‌گیرد

چرا کامپایل‌شدن یک یونیت هیچ را ثابت نمی‌کند؟

چون یک یونیت پاسکال می‌تواند به سمبلی ارجاع دهد که هرگز کار مفیدی نخواهد کرد و همچنان کامپایلر را راضی کند. در لحظه‌ای که هر ۱۱۳ یونیت کتابخانه زیر Free Pascal تمیز بیلد شد، handlerهای ظرف آرشیو واقعاً کار می‌کردند، راستی‌آزمایی‌شده با یک smoke test که یک CBZ باز و به PDF تبدیل می‌کرد. مسطح‌سازی فرم XFA اصلاً کار نمی‌کرد، چون مسطح‌سازی باید جریان بسته /XFA را inflate کند و نقطه ورود deflate هنوز یک stub بود. هیچ‌چیز در خروجی بیلد آن دو حالت را از هم تفکیک نمی‌کرد

قاعده‌ای که از آن بیرون آمد کوتاه است. پیش از نوشتن در یک release note اینکه یک ویژگی روی toolchain جدید کار می‌کند، یک پروب زمان اجرا بنویسید که آن ویژگی را سرتاسر روی همان toolchain ورز می‌دهد. پوشش کامپایل پیش‌نیاز است، هرگز مدرک نه. تصویر کلی‌تر از آنچه پورت پوشش می‌دهد در یادداشت‌های پشتیبانی Win64 برای Free Pascal و Lazarus است

یک raise درون stub cdecl به فراخواننده نمی‌رسد

این یکی بخش خودش را می‌طلبد چون نشانه‌اش به‌شدت گمراه‌کننده است. یونیت‌های stub نقاط ورود C را همان‌طور که یک کتابخانه استاتیک آشکار می‌کنند، پس یک stub این‌شکل است

// موجه به‌نظر می‌رسد. نیست.
function inflate(Strm: Pointer; Flush: Integer): PtrUInt; cdecl;
  public name 'inflate';
begin
  raise ENotSupportedException.Create('codec unavailable');
end;

روی Free Pascal برای Win64 آن استثنا به فراخواننده منتشر نمی‌شود. هیچ handler try..exceptای آن را نمی‌بیند، چون بازشدن روی مرز cdecl که این‌شکل اعلان شده قاب استثنای پاسکال را حمل نمی‌کند؛ فرایند با کد خروج 217 خاتمه می‌یابد. از سمت برنامه نه خطایی هست، نه پیامی و نه خطی در لاگ، فقط برنامه‌ای که ناپدید می‌شود. این اکیداً بدتر از یک پاسخ غلط است، چون پاسخ غلط قابل مدیریت است

چرا استثنایی که درون stub cdecl پرتاب می‌شود فرایند Free Pascal را با کد خروج 217 تمام می‌کند و چگونه کنترل‌کردن نقطه ورود پاسکال آن را fix می‌کند
قاب استثنای پاسکال نمی‌تواند روی مرز cdecl باز شود، پس فرایند بی‌سروصدا می‌میرد؛ fix پیش از آنکه به stub برسیم کنترل می‌گذارد

fix وسوسه‌انگیز این است که stub به‌جایش یک کد شکست برگرداند، و برای inflate درست است چون zlib بازگشت خطای خوش‌تعریفی دارد. به‌طور کلی غلط است: stubای برای jpeg_read_header که صفر برگرداند به فراخواننده می‌گوید با ساختاری ادامه دهد که هیچ‌کس مقداردهی‌اش نکرده. fix بادوام کنترل‌گذاری در نقطه ورود پاسکال است نه درون stub شکل-C، با استفاده از هر قرارداد شکستی که آن API از قبل دارد

function TryDecodeJPEG(const Data: TBytes; out Bitmap: TBitmap): Boolean;
begin
{$IFDEF FPC}
  // پیش از آنکه اصلاً به stub برسیم رد کن، با قرارداد شکست خودِ
  // این API نه با یک استثنا روی مرز cdecl
  Bitmap := nil;
  Result := False;
  Exit;
{$ENDIF}
  Result := DecodeJPEGNative(Data, Bitmap);
end;

paszlib همان zlib نیست، و تفاوتش دو رده سند است

پیاده‌سازی پاسکالی deflate موجود روی Free Pascal دو قاب‌بندی را هندل می‌کند: پوشش zlib و deflate خام. قاب‌بندی gzip را هندل نمی‌کند، که zlib از طریق مقادیر windowBits از 16 تا 31 انتخاب می‌شود، و حالت تشخیص خودکار را هندل نمی‌کند که مقادیر 32 تا 47 انتخابش می‌کنند. HotPDF هر دو را لازم دارد. مسیر امن واردات SVG عدد 31 را می‌خواهد، و لودر نردبان fallbackای دارد که وقتی قاب‌بندی یک جریان مبهم است عدد 47 را می‌خواهد. هر کدام را رد کنید و یک خانواده کامل از اسناد از بازشدن می‌افتد، با خطای decodeای که به جریان اشاره می‌کند نه به قاب‌بندی غایب

پوشش windowBits در paszlib در برابر zlib: بازه‌های قاب‌بندی gzip و تشخیص خودکار غایب برای واردات SVG در HotPDF و fallback لودر
paszlib پوشش zlib و deflate خام را هندل می‌کند، ولی HotPDF هم windowBits 31 و هم 47 را لازم دارد، پس shim باید خودش قاب‌بندی gzip را تأمین کند

ناسازگاری دوم تیزتر است. رکورد z_stream که paszlib اعلان می‌کند همان چیدمان حافظه نسخه C را ندارد: فیلد msg آن short string است نه اشاره‌گر، و total_in و total_out هشت‌بایتی‌اند جایی که ABI سی کلمات ماشین دارد. پس رکورد فراخواننده نمی‌تواند مستقیم از آن عبور کند. چیدمان کاری این است که وضعیت paszlib پشت اشاره‌گر state نگه داشته شود که رکورد عمومی از قبل رزرو کرده، و فیلدهای عمومی حوالی هر فراخوانی به درون و بیرون کپی شوند. CRC مربوط به gzip و تریلر طول هشت‌بایتی در همان لایه shim محاسبه می‌شوند، که جای طبیعی‌شان است چون خودش تصمیم قاب‌بندی را در اختیار دارد

پاس‌دادن آرایه پویا به یک پارامتر var بدون نوع

این باگی است که همین حالا به احتمال زیاد در کد شما نشسته. وقتی یک آرایه پویا به یک پارامتر var بدون نوع پاس می‌دهید، چیزی که callee دریافت می‌کند آدرس متغیر آرایه است، که آدرس یک اشاره‌گر است نه آدرس payload. پس خواندن درون آن خود متغیر و هر چه کنارش نشسته را بازنویسی می‌کند

var
  FBuffer: TBytes;
begin
  SetLength(FBuffer, 65536);

  // غلط: آدرس متغیر FBuffer را تحویل می‌دهد
  FStream.Read(FBuffer, Length(FBuffer));

  // درست: آدرس اولین بایت payload را تحویل می‌دهد
  FStream.Read(FBuffer[0], Length(FBuffer));
end;

روی Delphi فرم غلط اغلب به‌نظر می‌رسد کار می‌کند، چون آنچه خراب می‌کند یک جایگاه stack مجاور است که بعدش هیچ‌کس نمی‌خواند. روی Free Pascal همان خط در اولین استفاده خطای دسترسی به حافظه می‌دهد. چیزی که دیدنش با چشم را این‌قدر سخت می‌کند این است که آرایه‌های استاتیک چنین مشکلی ندارند، چون متغیر آرایه استاتیک خودش payload است، پس هر دو املای کد در همان فایل بسته به اعلانی چند صد خط دورتر درست‌اند

ظرف‌های ZIP بدون System.Zip

Free Pascal معادلی برای یونیت zip در RTL ندارد، و جایگزین موجود هم سطح API متفاوتی دارد هم پشتیبانی رمزگذاری قدیمی که فرمت‌های ظرف قدیمی‌تر هنوز استفاده می‌کنند را ندارد، پس یک reader کوچک درون کتابخانه کوتاه‌تر از سازگارشدن با آن از آب درآمد. دو جزئیات فرمت وقت بردند و به‌آسانی می‌توان غلطشان گرفت

نخستین بایت کنترل هدر رمزگذاری است. بایت دوازدهمش معمولاً بایت بالای CRC است، اما وقتی بیت 3 پرچم همه‌منظوره ست شده، یعنی اندازه‌ها در یک data descriptor انتهایی زندگی می‌کنند و CRC هنوز معلوم نیست، بایت کنترل به‌جایش از بایت بالای زمان تغییر می‌آید. فقط فرم CRC را پیاده کنید و هر آرشیوی که در حالت streaming نوشته شده یک رمز عبور درست را رد می‌کند. دومین فیلد اضافی ZIP64 است: سه فیلد هشت‌بایتی‌اش با ترتیب ثابتی ظاهر می‌شوند ولی فقط وقتی نوشته می‌شوند که فیلد ۳۲بیتی متناظر اشباع شده باشد، پس خواندن‌شان در آفست‌های ثابت روی آرشیوهایی که تست کردید کار می‌کند و روی بعدی شکست می‌خورد. آن‌ها را موضعی، بسته به اینکه کدام فیلدهای ۳۲بیتی اشباع شده‌اند، تجزیه کنید

یک آسان‌گیری ارزش دانستن: جریان بازکردن فشرده Free Pascal آرگومان سازنده دومی می‌گیرد که هدر zlib را رد می‌کند، که دقیقاً همان چیزی است که entryهای ZIP لازم دارند چون deflate خام ذخیره می‌کنند. آن مسیر اصلاً به shim zlib کتابخانه دست نمی‌زند، پس از بک‌اند C غایب بی‌تأثیر است

شفافیت glyph رنگی زیر LCL

خواندن کانال alpha یک glyph رنگی شطرنجی‌شده تنها جزئیات گرافیکی بدون ترجمه مستقیم است. کلاس PNG در LCL هیچ accessorای برای scanline ندارد که alpha را آشکار کند، و انتساب یک PNG به یک bitmap آن را دور می‌ریزد، پس یک ایموجی رنگی کاملاً opaque می‌رسد و با یک جعبه سیاه پشتش ترکیب می‌شود. مسیر کاری تصویر اینترفیسی است: از روی PNG بسازید، سپس پیکسل‌ها را از طریق accessor رنگ بخوانید، با یادآوری اینکه مؤلفه‌هایش ۱۶بیتی‌اند و برای بایت شدن باید هشت بیت به پایین شیفت شوند. آن سطح هم ترتیب سطر طبیعی بالا-به-پایین را به کار می‌برد، پس وارون‌سازی Height - 1 - Y که کد scanline در VCL لازم دارد باید حذف شود نه پورت

دو یادداشت سامانه بیلد پیش از ثبت باگ

یک بیلد کامل گاهی با سمبل تعریف‌نشده‌ای شکست می‌خورد که نامش به پسوند $crc و یک مقدار hex ختم می‌شود. آن پسوند از نوع‌های پارامتر محاسبه می‌شود، و وقتی مطابقت نمی‌کند که یک بیلد در همان گذر یک یونیت را در برابر دو نسخه متفاوت interface کامپایل کرده باشد. اجرای دوباره بیلد پاکش می‌کند؛ امضا غلط نیست

دوم، Free Pascal 3.2.2 متد بی‌نام ندارد، پس هر جا کتابخانه برای سیم‌کشی یک خط لوله موازی از closureها استفاده می‌کرد بیلد Free Pascal به‌جایش یک fallback سریال قطعیت‌مند برمی‌دارد. خروجی یکسان است، توان عبوری نه؛ اگر به رندر موازی صفحه وابسته‌اید، آن دلیلی است برای اینکه فعلاً روی Delphi بمانید، و طراحی خط لوله در مقاله خط لوله رندر موازی توصیف شده است. وضعیت کدک تصویر جای دیگری است که انتخاب toolchain قابلیت را تغییر می‌دهد نه فقط سرعت را، پس یک استقرار Lazarus باید فرمت‌های تصویرش را بر همان اساس برنامه‌ریزی کند؛ ماتریس فعلی به‌ازای هر toolchain در صفحه محصول HotPDF Delphi PDF component است