מאמר טכני

HotPDF ב-Free Pascal: Deflate, AES ומגבלות קודקים

HotPDF מתקמפל ורץ תחת Free Pascal 3.2.2 עם Lazarus, והסיכום הכנה של ההעברה הזאת הוא שתי משפטים. יצירת מסמכים, טעינה, שמירה, דחיסה, פריסת דחיסה, הצפנה ופענוח הכול עובדים על backends פסקליים בלבד, כך שאפליקציית Lazarus יכולה להפיק ולצרוך PDF אמיתי בלי כל תלות ב-C. הקודקים המקוריים האופציונליים לתמונות לא, כי ה-objects של Win64 שנבנו מראש משתמשים בטעם COFF שאף אחד מהמקשרים של Free Pascal לא מסוגל לצרוך, ולכן ב-toolchain הזה נקודות הכניסה נפתרות ל-stubs שמתנהגים fail closed

מפת יכולות של HotPDF ב-Free Pascal: backends פסקליים עובדים של deflate, AES ומסמכים לצד stubs של קודקי תמונה שמתנהגים fail closed
תכונות המסמך, הדחיסה וההצפנה רצות על backends פסקליים בלבד, בזמן שקודקי תמונה מקוריים נפתרים ל-stubs שמתנהגים fail closed

המעבר מ"מתקמפל" ל"עובד" לקח קבוצת תיקונים מסוימת, וכל אחד מהם הוא מלכודת שתמצא כל בסיס קוד אחר של Delphi שעובר ל-Free Pascal. שווה לרשום אותם לפי סדר הכאב שלהם

למה זה שיחידה מתקמפלת לא מוכיח כלום?

כי יחידת פסקל יכולה להפנות לסימבול שלעולם לא יעשה דבר שימושי ועדיין לספק את המהדר. בנקודה שבה כל 113 יחידות הספרייה נבנו נקי תחת Free Pascal, מטפלי מיכלי הארכיון באמת עבדו, מאומתים על ידי בדיקת עשן שפתחה CBZ והמירה אותה ל-PDF. הישור של טפסי XFA לא עבד בכלל, כי הישור צריך לנפח את stream ה-/XFA הדחוס ונקודת הכניסה של ה-deflate הייתה עדיין stub. שום דבר בפלט הבנייה לא הבחין בין שני המקרים

הכלל שיצא מזה קצר. לפני שכותבים בהודעת גרסה שתכונה עובדת על toolchain חדש, כתבו בדיקת runtime שמפעילה את התכונה מקצה לקצה על אותו toolchain. כיסוי קימפול הוא תנאי מוקדם, לעולם לא ראיה. התמונה הרחבה של מה שההעברה מכסה נמצאת בהערות התמיכה של Free Pascal ו-Lazarus ב-Win64

זריקה בתוך 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 החריגה הזאת אינה מתפשטת אל הקורא. אין מטפל try..except שרואה אותה, כי פריקה מעל גבול cdecl שהוכרז בדרך הזאת אינה נושאת את מסגרת החריגה של פסקל; התהליך מסתיים עם קוד יציאה 217. מצד האפליקציה אין שגיאה, אין הודעה ואין שורת יומן, רק תוכנית שנעלמת. זה בהחלט גרוע מתשובה שגויה, כי תשובה שגויה אפשר לטפל בה

מדוע חריגה שנזרקת בתוך stub של cdecl מסיימת תהליך Free Pascal עם קוד יציאה 217 וכיצד שער בנקודת הכניסה הפסקלית מתקן זאת
מסגרת החריגה הפסקלית אינה מסוגלת להיפרק מעל גבול ה-cdecl, ולכן התהליך מת בשקט; התיקון שוער לפני שה-stub מושג אי פעם

התיקון המפתה הוא לגרום ל-stub להחזיר קוד כשל במקום זאת, ועבור inflate זה נכון כי ל-zlib יש החזרת שגיאה מוגדרת היטב. זה שגוי באופן כללי: stub עבור jpeg_read_header שמחזיר אפס אומר לקורא להמשיך עם מבנה שאף אחד לא אתחל. התיקון העמיד הוא לשער בנקודת הכניסה הפסקלית ולא בתוך ה-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 כשהמסגרת של stream עמומה. לדלג על אחד מהם ומשפחה שלמה של מסמכים מפסיקה להיפתח, עם שגיאת פענוח שמצביעה על ה-stream ולא על המסגרת החסרה

כיסוי windowBits של paszlib מול zlib: מסגרת gzip וטווחי זיהוי אוטומטי חסרים עבור יבוא SVG של HotPDF ו-fallback של הטוען
paszlib מטפל בעוטף ה-zlib וב-deflate גלמי, אבל HotPDF צריך גם windowBits 31 ו-47, ולכן ה-shim חייב לספק בעצמו את מסגרת ה-gzip

יש אי התאמה שנייה וחדה יותר. ה-record z_stream ש-paszlib מצהיר אינו בעל אותה פריסת זיכרון כמו של C: השדה msg שלו הוא short string ולא מחוון, ו-total_in ו-total_out הם בני 64 ביט במקום שבו ל-ABI של C יש מילות מכונה. record של קורא לכן לא יכול להיות מועבר ישירות. הסידור העובד הוא להחזיק את מצב ה-paszlib מאחורי המחוון state שה-record הציבורי כבר שומר, ולהעתיק את השדות הציבוריים פנימה והחוצה סביב כל קריאה. CRC של ה-gzip ו-trailer האורך בן שמונה הבייטים מחושבים באותה שכבת shim, שהיא המקום הטבעי להם כי היא כבר בעלת החלטת המסגור

העברת מערך דינמי לפרמטר var חסר טיפוס

זה הבאג שהסביר ביותר שהוא יושב בקוד שלכם עכשיו. כשמעבירים מערך דינמי לפרמטר var חסר טיפוס, מה שהנערן מקבל הוא הכתובת של משתנה המערך, שהיא הכתובת של מחוון, ולא הכתובת של ה-payload. קריאה אל תוכו לכן דורסת את המשתנה עצמו ואת מה שיושב לידו

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

  // שגוי: מוסר את הכתובת של המשתנה FBuffer
  FStream.Read(FBuffer, Length(FBuffer));

  // נכון: מוסר את הכתובת של בייט ה-payload הראשון
  FStream.Read(FBuffer[0], Length(FBuffer));
end;

ב-Delphi הצורה השגויה לעיתים קרובות נראית כעובדת, כי מה שהיא משחיתה הוא תא מחסנית סמוך ששום דבר לא קורא אחר כך. ב-Free Pascal אותה שורה מקבלת segmentation fault בשימוש הראשון. מה שהופך זאת קשה כל כך לאיתור בעין הוא שלמערכים סטטיים אין בעיה כזאת, כי משתנה של מערך סטטי הוא ה-payload של עצמו, ולכן שתי הצורות נכונות באותו קובץ בהתאם להצהרה כמה מאות שורות קדימה

מיכלי ZIP בלי System.Zip

ל-Free Pascal אין מקבילה ליחידת ה-zip של ה-RTL, והחלופה הזמינה היא גם בעלת משטח API אחר וגם חסרה תמיכה בהצפנה הישנה שפורמטי מיכל עתיקים עדיין משתמשים בה, ולכן קורא קטן בתוך הספרייה התברר כקצר יותר מהתאמה אליה. שני פרטי פורמט עלו בזמן וקל לטעות בהם

הראשון הוא בייט הבדיקה של כותרת ההצפנה. הבייט השנים עשר שלו הוא בדרך כלל הבייט הגבוה של ה-CRC, אבל כשביט 3 של הדגל הכללי דלוק, כלומר הגדלים נמצאים ב-data descriptor עוקב וה-CRC עדיין אינו ידוע, בייט הבדיקה בא מהבייט הגבוה של שעת השינוי במקום. לממש רק את הצורה של ה-CRC וכל ארכיון שנכתב במצב streaming דוחה סיסמה נכונה. השני הוא שדה ה-extra של ZIP64: שלושת השדות שלו בני 64 ביט מופיעים בסדר קבוע אבל נכתבים רק כשהשדה המתאים בן 32 הביט רווי, ולכן קריאתם בהיסת קבועים עובדת על הארכיונים שבדקתם ונכשלת על הבא בתור. נתחו אותם מבחינה מיקומית לפי אילו שדות בני 32 ביט רוויים

נוחות אחת ששווה להכיר: stream הפריסה של Free Pascal מקבל ארגומנט constructor שני שמדלג על כותרת ה-zlib, וזה בדיוק מה שרשומות ZIP צריכות כי הן מאחסנות deflate גלמי. הנתיב הזה לא נוגע כלל ב-zlib shim של הספרייה, ולכן אינו מושפע מה-backend של C החסר

שקיפות glyph צבעוני תחת ה-LCL

קריאת ערוץ ה-alpha של glyph צבעוני מרוסטר היא פרט הגרפיקה האחד בלי תרגום ישיר. למחלקת ה-PNG של ה-LCL אין מאחזר scanline שחושף alpha, והצבת PNG ל-bitmap משליכה אותו, ולכן emoji צבעוני מגיע אטום לגמרי ומורכב עם תיבה שחורה מאחוריו. הנתיב העובד הוא תמונת ה-interface: יוצרים אותה מה-PNG, ואז קוראים פיקסלים דרך המאחזר הצבעוני, זוכרים שרכיביו בני 16 ביט וזקוקים להזזה מטה בשמונה כדי להפוך לבייטים. אותו משטח גם משתמש בסדר שורות טבעי מלמעלה למטה, ולכן את ההיפוך Height - 1 - Y שקוד scanline של VCL צריך יש להסיר ולא להעביר

שתי הערות מערכת בנייה לפני שפותחים באג

בנייה מלאה מדי פעם נכשלת עם סימבול לא מוגדר ששמו מסתיים בסיומת $crc וערך הקסדצימלי. הסיומת הזאת מחושבת מטיפוסי הפרמטרים, והיא נכשלת בהתאמה כשבנייה אחת מקמפלת יחידה מול שתי גרסאות interface שונות באותו מעבר. ריצה חוזרת של הבנייה מנקה זאת; החתימה אינה שגויה

שנית, ל-Free Pascal 3.2.2 אין שיטות אנונימיות, ולכן בכל מקום שבו הספרייה השתמשה ב-closures כדי לחבר צינורית מקבילית, הבנייה של Free Pascal לוקחת fallback סדרתי דטרמיניסטי במקום. הפלט זהה, התפוקה לא; אם אתם תלויים ברינדור עמודים מקבילי, זו סיבה להישאר על Delphi בינתיים, ועיצוב הצינורית מתואר במאמר צינורית הרינדור המקבילית. מצב קודקי התמונה הוא המקום השני שבו בחירת toolchain משנה יכולת ולא רק מהירות, ולכן פריסת Lazarus צריכה לתכנן את פורמטי התמונות שלה בהתאם; המטריצה הנוכחית לפי toolchain נמצאת בדף המוצר של HotPDF Delphi PDF component