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, שמעתיקה אותה שוב לתוך המאגר הפרטי של הרכיב. שתי העתקות של אותם בייטים, אחת מהן טקס טהור, וזה חוזר על עצמו בכל הודעה בסריקת תיבת-דואר גדולה. עומס-היתר של טווח-הבייטים מסיר את ההעתקה הראשונה ללא תנאי ואת השנייה אופציונלית
מה עומס-היתר של טווח-הבייטים בעצם עושה
העומס-יתר דק בכוונה: הוא מאמת, מחשב מצביע אחד, ומאציל לצורת-המצביע של LoadDocument שכל המשפחה כבר עוברת דרכה. Index מבוסס-אפס, Count הוא אורך בייטים, ו-Buffered ברירת המחדל שלה True בדיוק כפי שהיא בעומסי-היתר האחרים. LoadDocument(const Data: TBytes; Buffered: Boolean) חד-הארגומנט עצמה כעת היא רק קריאה לזו עם Index = 0 ו-Count = Length(Data), כך שיש נתיב אימות אחד במקום שניים
קריאה לה נראית כמו הקוד שכבר כתבת, מינוס הפרוסה
var
Frame: TBytes; // whole container record, tens of megabytes
Offset, Size: Integer;
begin
Frame := LoadContainerRecord('mailbox.dat');
LocateEmbeddedPdf(Frame, Offset, Size); // your container parser
// No Copy(Frame, Offset, Size) here - the window is addressed in place
Pdf.LoadDocument(Frame, Offset, Size, True);
try
RenderPreview(Pdf);
finally
Pdf.UnloadDocument;
end;
end;
למה Index פלוס Count גולש בבדיקת הגבולות?
מפני ש-Index ו-Count שניהם Integer, וסכום שני ערכי Integer חיוביים גדולים לא בהכרח Integer חיובי גדול. זה הליבה הטכנית של העומס-יתר, וזה המקום היחיד שבו בדיקה שנראית טבעית היא חור בטיחות-זיכרון. הניסוח הברור שגוי
// WRONG: Index + Count is evaluated in Integer and can wrap negative
if Index + Count <= Length(Data) then
DataPtr := @Data[Index];
// RIGHT: reject signs first, then bound each term separately,
// with the only arithmetic done as a subtraction that cannot wrap
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 ובטיחות זיכרון בדלפי
למה חלון באורך אפס חייב להעביר 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; // owns the backing storage for as long as FPdf is loaded
FPdf: TPdf;
public
procedure OpenEmbedded(Offset, Size: Integer);
destructor Destroy; override;
end;
procedure TFrameSession.OpenEmbedded(Offset, Size: Integer);
begin
// Buffered = False: FFrame must outlive the loaded document
FPdf.LoadDocument(FFrame, Offset, Size, False);
end;
destructor TFrameSession.Destroy;
begin
FPdf.UnloadDocument; // release the borrow first
FFrame := nil; // only now may the storage go
inherited;
end;
כשחלון טווח-הבייטים הוא הכלי השגוי
היה כן לגבי הגבול. עומס-היתר של טווח-הבייטים מניח שהקונטיינר כבר לגמרי בזיכרון, ו-Count הוא Integer, כך שחלון בודד לא יכול לחרוג משני-גיגה-בייט. אם הקונטיינר הוא ארכיון בן 6 GB על הדיסק, או מגיע דרך socket שאתה לא יכול להריץ אחורה, העומס-יתר הזה לא יכול לעזור לך וקריאת כל הדבר לתוך TBytes רק כדי לפנות לחלון בתוכו מבטלת את הטעם. שם בדיוק שייך נתיב FPDF_FILEACCESS, ומאמר ההזרמה לפי-דרישה מראה כיצד לחשוף תצוגה מוסטת-היסט של קובץ כמקור מסמך מותאם אישית. באותה מידה, אם הבייטים המשוטמעים זקוקים לטרנספורמציה לפני ש-PDFium רואה אותם, פענוח, פענוח-הצפנה, שלב הסרת-עטיפה, אז העתקה אמיתית בלתי-נמנעת ו-Buffered = True על המערך המומר היא התשובה הכנה. חלון טווח-הבייטים משתלם בדיוק בצורה אחת: בייטי PDF רציפים, לא-משתנים, כבר-חיים, בהיסט ידוע
אם אתה מעריך זאת עבור צופה, חלונית תצוגה-מקדימה, או pipeline קליטת-אצווה, עומס-היתר של טווח-הבייטים וטוען ההזרמה הם שתיים מאסטרטגיות הטעינה ש-PDFium Component שולחת לצד טעינות קובץ, זרם ומצביע גולמי. משטח ה-API המלא, הרישוי ותמיכת גרסאות דלפי ו-C++Builder מתועדים בעמוד המוצר PDFium Component