מאמר טכני

טעינת טווח-בייטים PDFium עבור PDF משוטמעים בדלפי

PDFium Component יכולה לפתוח PDF שחי בתוך מאגר גדול יותר ישירות מטווח-בייטים. העומס-יתר LoadDocument(const Data: TBytes; Index, Count: Integer; Buffered: Boolean) פונה לחלון במקום, כך שאין צורך ב-Copy מקדים. בתמורה הוא מבקש שתבין כלל אחד: כש-Buffered הוא False, המערך הבסיסי מושאל, לא מועתק

זהו מנגנון שונה מהגישה מונעת-callback המתוארת בהזרמת PDF גדולים לפי דרישה עם PDFium VCL, שמוסרת ל-PDFium קורא FPDF_FILEACCESS ונותנת לה למשוך בלוקים מהדיסק לפי הצורך שלה. זה מיועד למסמכים גדולים מכדי להחזיק ב-RAM כלל. זה מיועד למסמכים שכבר ב-RAM, יושבים בהיסט ידוע בתוך משהו אחר. השניים משלימים, והחלק האחרון מסביר לאיזה מצב איזה שייך

ההעתקה בת ה-40 מגה-בייט שאף אחד לא ביקש

התרחיש מופיע בכל מקום שקבצי PDF נוסעים בתוך פורמטים אחרים. חנות דואר שומרת גופי הודעות וקבצים מצורפים ברשומה אחת. קונטיינר ארכיון משרשר manifest, כמה תמונות ו-PDF. פרוטוקול חיווט מותאם אישית ממסגר מסמך מאחורי כותרת שיש בה קידומת-אורך. בכל מקרה אתה נותר מחזיק TBytes גדול אחד ויודע ש-ה-PDF מתחיל בבייט 1,182,336 ורץ למשך 312 קילו-בייט

לפני שהעומס-יתר של טווח-הבייטים היה קיים, התשובה האידיומטית הייתה Copy(Data, Index, Count), שמקצה מערך שני ומבצעת memcpy של החלון לתוכו. אתה אז מוסר את הפרוסה הזו ל-LoadDocument עם Buffered = True, שמעתיקה אותה שוב לתוך המאגר הפרטי של הרכיב. שתי העתקות של אותם בייטים, אחת מהן טקס טהור, וזה חוזר על עצמו בכל הודעה בסריקת תיבת-דואר גדולה. עומס-היתר של טווח-הבייטים מסיר את ההעתקה הראשונה ללא תנאי ואת השנייה אופציונלית

תרשים השוואה המציג את נתיב ההעתקה הכפול הישן עבור PDF מוטמע בתוך מיכל ב-Delphi מול ההעמסה LoadDocument של טווח בייטים ב-PDFium Component שפונה אל החלון במקום
בלי ה-overload אותו חלון של 312 KB הועתק פעמיים ב-memcpy; קריאת ה-byte-range פונה אליו במקום ו-Buffered מכריעה אם משהו מועתק כלל

מה עומס-היתר של טווח-הבייטים בעצם עושה

העומס-יתר דק בכוונה: הוא מאמת, מחשב מצביע אחד, ומאציל לצורת-המצביע של LoadDocument שכל המשפחה כבר עוברת דרכה. Index מבוסס-אפס, Count הוא אורך בייטים, ו-Buffered ברירת המחדל שלה True בדיוק כפי שהיא בעומסי-היתר האחרים. LoadDocument(const Data: TBytes; Buffered: Boolean) חד-הארגומנט עצמה כעת היא רק קריאה לזו עם Index = 0 ו-Count = Length(Data), כך שיש נתיב אימות אחד במקום שניים

קריאה לה נראית כמו הקוד שכבר כתבת, מינוס הפרוסה

var
  Frame: TBytes;          // רשומת המכל כולה, עשרות מגה-בייטים
  Offset, Size: Integer;
begin
  Frame := LoadContainerRecord('mailbox.dat');
  LocateEmbeddedPdf(Frame, Offset, Size);   // המנתח של המכל שלכם

  // אין Copy(Frame, Offset, Size) כאן - החלון מוען במקום
  Pdf.LoadDocument(Frame, Offset, Size, True);
  try
    RenderPreview(Pdf);
  finally
    Pdf.UnloadDocument;
  end;
end;

למה Index פלוס Count גולש בבדיקת הגבולות?

מפני ש-Index ו-Count שניהם Integer, וסכום שני ערכי Integer חיוביים גדולים לא בהכרח Integer חיובי גדול. זה הליבה הטכנית של העומס-יתר, וזה המקום היחיד שבו בדיקה שנראית טבעית היא חור בטיחות-זיכרון. הניסוח הברור שגוי

// שגוי: Index + Count מוערך כ-Integer ויכול לגלוש לשלילי
if Index + Count <= Length(Data) then
  DataPtr := @Data[Index];

// נכון: דחו סימנים תחילה, ואז הגבילו כל איבר בנפרד,
// כאשר האריתמטיקה היחידה מתבצעת כחיסור שלא יכול לגלוש
Check(Index >= 0,  'PDF byte range index cannot be negative');
Check(Count >= 0,  'PDF byte range count cannot be negative');
Check(Index <= Length(Data), 'PDF byte range index exceeds data length');
Check(Count <= Length(Data) - Index, 'PDF byte range exceeds data length');

עבוד את המקרה הכושל. קח Index = 2000000000 ו-Count = 2000000000. הסכום האמיתי שלהם הוא ארבעה מיליארד, אבל באריתמטיקה חתומה בת 32-סיביות התוצאה עוטפת בדיוק ל-מינוס 294,967,296. הערך הזה קטן בנוחות מ-Length(Data), כך שהבדיקה השגויה עוברת, @Data[Index] נלקח הרבה מחוץ למערך, ו-PDFium מקבלת מצביע פרוע בתוספת אורך של שני-גיגה-בייט. מה שקורה אחר-כך הוא access violation ביום טוב וניתוח שקט של זיכרון תהליך לא-קשור ביום רע

הסדר הנכון מתקן זאת על ידי לעולם לא לחבר. שליליים נדחים לפני שדבר-כלשהו נעשה אינדקס, כך ש-@Data[Index] אף פעם לא יכול להילקח מתחת למערך. אז Index מוגבל בפני עצמו כנגד Length(Data), מה שמבטיח ש-Length(Data) - Index הוא Integer לא-שלילי. רק אז Count מושווה כנגד השארית הזו. כל ערך ביניים נשאר בתוך הטווח הניתן-לייצוג, כך ששום תצורת בנייה לא יכולה לשנות את התוצאה. אל תתפתה להסתמך על בדיקת גלישת {$Q+} כרשת ביטחון גם כן: בניות שחרור בדרך כלל נשלחות איתה כבויה, וגם כשהיא דלוקה, הפכת באג בטיחות-זיכרון ל-EIntOverflow שבורח מאמצע שגרת אימות. PDFium Component מתייחסת לאריתמטיקת אורך לא-מהימנה באותה דרך שהיא מתייחסת לשאר הגבול, משמעת שמכוסה בהרחבה בחיזוק ה-ABI של PDFium VCL ובטיחות זיכרון בדלפי

תרשים זרימה מנגד את בדיקת הגבולות של Index בתוספת Count הגולשת מול סדר האימות של חיסור בלבד השומר על טעינת טווח בייטים של PDFium בטוחת-זיכרון ב-Delphi
חיבור תחילה עוטף מעבר ל-MaxInt ומביס את השומר; דחיית סימנים ותחימת איברים לפני חיסור אחד חף-מגלישה שומר על כל ביניים ניתן-לייצוג

למה חלון באורך אפס חייב להעביר nil?

מפני ש-@Data[Index] אינו ביטוי חוקי לכל Index שהאימות מקבל. Index = Length(Data) עם Count = 0 הוא חלון ריק תקין-לגמרי בקצה מאגר, ו-TBytes ריק נותן Index = 0 על מערך שאין לו אלמנט אפס כלל. לקיחת הכתובת בכל אחד מהמקרים עושה אינדקס מעבר לסוף, או dereference של מערך דינמי nil. אז העומס-יתר מסתעף: Count = 0 מניב מצביע nil, כל count אחר מניב @Data[Index]. ה-nil אז זורם לתוך עומס-היתר של המצביע, שההגנה שלו עצמה מקבלת מצביע nil כשהגודל אפס, וההעברה מסתיימת בשגיאת "Cannot load PDF document" הרגילה במקום access violation. קורא שחישב חלון בן-אפס-בייטים ממיכל פגום מקבל EPdfError נקי וניתן-ללכידה כמו כל קלט שגוי אחר

מושאל או מועתק: מה ש-Buffered מחליט

Buffered בוחרת את חוזה הבעלות, וזה הפרמטר היחיד כאן עם השלכות מעבר לקריאה עצמה. עם Buffered = True, PDFium Component מעתיקה את החלון הנבחר, ורק אותו, לתוך המאגר הפנימי שלה לפני הטעינה. ה-40 מגה-בייט קונטיינר לא מועתקים; ה-PDF בן 312 KB כן. ברגע ש-LoadDocument חוזרת אתה יכול לשחרר, לעשות שימוש-חוזר, או לדרוס את הקונטיינר מיד, כי הרכיב כבר לא מפנה אליו. זו ברירת המחדל והבחירה הנכונה עבור כמעט כל קוד

Buffered = False מעבירה @Data[Index] ישירות ל-FPDF_LoadMemDocument64, ו-PDFium שומרת את המצביע ההוא לאורך חיי המסמך במקום להעתיק את הבייטים. זה הופך את הטעינה ללא-הקצאה, וזה הופך את כל TBytes הבסיסי למשאב מושאל. הוא חייב להישאר חי ולא-משתנה עד ש-UnloadDocument רצה או Active הופך False. לא החלון, המערך כולו: מערך דינמי נספר-הפניות כיחידה, ושחרור ההפניה האחרונה בכל מקום בקוד שלך משחרר את הזיכרון ש-PDFium עדיין קוראת ממנו. קביעת Length עליו קטלנית באותה מידה, כי realloc יכול להזיז את הבלוק. הצהר על זה בתיעוד ה-API שלך עצמך בכל מקום שאתה חושף טעינה כזו, באותה רוח כמו כל גבול השאלה-מול-בעלות אחר בקוד Pascal; מצב הכישלון זהה לסכנות ה-aliasing המתוארות בדליפת FillChar ומחרוזת-תוצאה בדלפי, שבו מאגר נראה בבעלות ואינו

type
  TFrameSession = class
  private
    FFrame: TBytes;   // הבעלים של האחסון התומך כל עוד FPdf טעון
    FPdf: TPdf;
  public
    procedure OpenEmbedded(Offset, Size: Integer);
    destructor Destroy; override;
  end;

procedure TFrameSession.OpenEmbedded(Offset, Size: Integer);
begin
  // Buffered = False: FFrame חייב לשרוד יותר מהמסמך הטעון
  FPdf.LoadDocument(FFrame, Offset, Size, False);
end;

destructor TFrameSession.Destroy;
begin
  FPdf.UnloadDocument;   // שחרור ההשאלה תחילה
  FFrame := nil;         // רק כעת האחסון רשאי ללכת
  inherited;
end;

כשחלון טווח-הבייטים הוא הכלי השגוי

היה כן לגבי הגבול. עומס-היתר של טווח-הבייטים מניח שהקונטיינר כבר לגמרי בזיכרון, ו-Count הוא Integer, כך שחלון בודד לא יכול לחרוג משני-גיגה-בייט. אם הקונטיינר הוא ארכיון בן 6 GB על הדיסק, או מגיע דרך socket שאתה לא יכול להריץ אחורה, העומס-יתר הזה לא יכול לעזור לך וקריאת כל הדבר לתוך TBytes רק כדי לפנות לחלון בתוכו מבטלת את הטעם. שם בדיוק שייך נתיב FPDF_FILEACCESS, ומאמר ההזרמה לפי-דרישה מראה כיצד לחשוף תצוגה מוסטת-היסט של קובץ כמקור מסמך מותאם אישית. באותה מידה, אם הבייטים המשוטמעים זקוקים לטרנספורמציה לפני ש-PDFium רואה אותם, פענוח, פענוח-הצפנה, שלב הסרת-עטיפה, אז העתקה אמיתית בלתי-נמנעת ו-Buffered = True על המערך המומר היא התשובה הכנה. חלון טווח-הבייטים משתלם בדיוק בצורה אחת: בייטי PDF רציפים, לא-משתנים, כבר-חיים, בהיסט ידוע

אם אתה מעריך זאת עבור צופה, חלונית תצוגה-מקדימה, או pipeline קליטת-אצווה, עומס-היתר של טווח-הבייטים וטוען ההזרמה הם שתיים מאסטרטגיות הטעינה ש-PDFium Component שולחת לצד טעינות קובץ, זרם ומצביע גולמי. משטח ה-API המלא, הרישוי ותמיכת גרסאות דלפי ו-C++Builder מתועדים בעמוד המוצר PDFium Component

תרשים ציר זמן של שני מצבי ה-Buffered בעת טעינת PDF מוטמע מטווח בייטים ב-Delphi: העתקת רק חלון ה-PDF ושחרור המיכל מיד מול השאלת ה-TBytes התומך כולו עד ש-UnloadDocument רץ
Buffered = True מעתיקה רק את החלון כך שהמכולה ניתנת-להשלכה, בזמן ש-Buffered = False משאירה את כל המערך התומך מושאל עד הפריקה