يستطيع PDFium Component فتح ملف PDF يعيش داخل مخزن مؤقت أكبر مباشرةً من نطاق بايت. الحمولة الزائدة LoadDocument(const Data: TBytes; Index, Count: Integer; Buffered: Boolean) تُعالج نافذة في مكانها، بحيث لا حاجة لـCopy تمهيدي. في المقابل تطلب منك فهم قاعدة واحدة: عندما تكون Buffered تساوي False، تكون المصفوفة الداعمة مُستعارة، لا منسوخة
هذه آلية مختلفة عن النهج المُدفوع بردود النداء الموصوف في بث ملفات PDF كبيرة عند الطلب باستخدام PDFium VCL، الذي يُسلّم لـPDFium قارئ FPDF_FILEACCESS ويدعه يسحب كتلًا من القرص عند الحاجة. تلك الآلية للمستندات الكبيرة جدًا بحيث لا تسعها الذاكرة العشوائية. هذه الآلية للمستندات الموجودة بالفعل في الذاكرة العشوائية، جالسة عند إزاحة معروفة داخل شيء آخر. الاثنتان متكاملتان، والقسم الأخير يشرح أي موقف ينتمي لأيهما
الأربعون ميجابايت من النسخ التي لم يطلبها أحد
السيناريو يظهر حيثما تسافر ملفات PDF داخل تنسيقات أخرى. مخزن بريد يحتفظ بنصوص الرسائل ومرفقاتها في سجل واحد. حاوية أرشيف تُلحق بيانًا، وبضع صور، وملف 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 مؤشر جامح بالإضافة إلى طول اثنَي غيغابايت. ما يتبع ذلك انتهاك وصول في يوم جيد وتحليل صامت لذاكرة عملية غير ذات صلة في يوم سيئ
الترتيب الصحيح يُصلح هذا بعدم الجمع أبدًا. القيم السالبة تُرفض قبل فهرسة أي شيء، بحيث لا يمكن أبدًا أخذ @Data[Index] تحت المصفوفة. ثم يُقيَّد Index بمفرده مقابل Length(Data)، مما يضمن أن Length(Data) - Index هي Integer غير سالبة. فقط بعد ذلك تُقارَن Count بذلك الباقي. كل قيمة وسيطة تبقى داخل النطاق القابل للتمثيل، بحيث لا يمكن لأي تهيئة بناء تغيير النتيجة. لا تُغرَّر بالاعتماد على فحص فيض {$Q+} كشبكة أمان أيضًا: بنيات الإصدار عادةً تُشحن معطلة، وحتى عندما تكون مفعّلة تكون قد حوّلت خللًا في سلامة الذاكرة إلى EIntOverflow يهرب من منتصف روتين تحقق. يعامل PDFium Component حساب الطول غير الموثوق بنفس الطريقة التي يعامل بها بقية الحدود، انضباط مشروح على نطاق أوسع في تحصين واجهة ABI لـPDFium VCL وسلامة الذاكرة في Delphi
لماذا يجب أن تُمرّر نافذة بطول صفر nil؟
لأن @Data[Index] ليس تعبيرًا شرعيًا لكل Index يقبله التحقق. Index = Length(Data) مع Count = 0 نافذة فارغة صحيحة البناء تمامًا عند ذيل المخزن المؤقت، وTBytes فارغة تُعطي Index = 0 على مصفوفة ليس لها عنصر صفر إطلاقًا. أخذ العنوان في أي من الحالتين يُفهرس بعد النهاية، أو يُلغي الإشارة إلى مصفوفة ديناميكية nil. لذا تتفرّع الحمولة الزائدة: Count = 0 يُنتج مؤشر nil، أي عدد آخر يُنتج @Data[Index]. الـnil بعدها يتدفق إلى الحمولة الزائدة للمؤشر، التي يقبل حارسها الخاص مؤشر nil عندما يكون الحجم صفرًا، وينتهي التحميل بخطأ "Cannot load PDF document" العادي بدلًا من انتهاك وصول. مستدعٍ حسب نافذة صفر بايت من حاوية مشوَّهة يحصل على EPdfError نظيف وقابل للالتقاط كأي مدخل سيء آخر
مُستعار أو منسوخ: ما تُقرره Buffered
تختار Buffered عقد الملكية، وهي الوسيط الوحيد هنا الذي له عواقب تتجاوز الاستدعاء. مع Buffered = True، ينسخ PDFium Component النافذة المختارة، والنافذة فقط، إلى مخزنه المؤقت الداخلي قبل التحميل. حاوية الأربعين ميجابايت لا تُنسخ؛ ملف PDF بحجم 312 كيلوبايت يُنسخ. بمجرد أن تعود LoadDocument، يمكنك تحرير الحاوية أو إعادة استخدامها أو الكتابة فوقها فورًا، لأن المكوّن لم يعد يشير إليها. هذا الافتراضي والاختيار الصحيح لتقريبًا كل الكود
Buffered = False يُمرِّر @Data[Index] مباشرةً إلى FPDF_LoadMemDocument64، ويحتفظ PDFium بذلك المؤشر طوال عمر المستند بدلًا من نسخ البايتات. هذا يجعل التحميل خاليًا من التخصيص، ويجعل TBytes الداعمة بأكملها موردًا مُستعارًا. يجب أن تبقى حية وغير مُعدَّلة حتى يعمل UnloadDocument أو تصبح Active تساوي False. ليس النافذة فحسب، بل المصفوفة بأكملها: مصفوفة ديناميكية معدودة المراجع كوحدة، والسماح بذهاب آخر مرجع في أي مكان بكودك يُحرّر الذاكرة التي لا يزال PDFium يقرأها. تعيين Length عليها مميت بنفس القدر، لأن إعادة التخصيص يمكن أن تُحرِّك الكتلة. اذكر هذا في توثيق واجهتك البرمجية الخاصة أينما تعرض تحميلًا كهذا، بنفس روح أي حد استعارة-مقابل-ملكية آخر في كود Pascal؛ نمط الفشل مطابق لمخاطر التسمية المستعارة الموصوفة في تسرّب FillChar وسلسلة النتيجة في Delphi، حيث يبدو مخزن مؤقت مملوكًا وليس كذلك
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 غيغابايت على القرص، أو تصل عبر مقبس لا يمكنك إرجاعه، لا تستطيع هذه الحمولة الزائدة مساعدتك وقراءة الشيء بأكمله إلى TBytes فقط لعنونة نافذة داخله يُبطل الغاية. هذا بالضبط حيث ينتمي مسار FPDF_FILEACCESS، ويُظهر مقال البث عند الطلب كيفية عرض عرض مُزاح الإزاحة لملف كمصدر مستند مخصص. بالمثل، إذا احتاجت البايتات المضمّنة تحويلًا قبل أن يراها PDFium، فك ضغط، فك تشفير، خطوة فك تغليف، فإن نسخة حقيقية لا مفر منها وBuffered = True على المصفوفة المُحوَّلة هي الإجابة الصادقة. نافذة نطاق البايت تجني ثمارها في شكل واحد بالضبط: بايتات PDF متجاورة غير مُعدَّلة، مقيمة بالفعل، عند إزاحة معروفة
إذا كنت تُقيّم هذا لعارض، أو لوحة معاينة، أو خط أنابيب استيعاب دفعي، فحمولة نطاق البايت الزائدة والمُحمِّل المتدفق استراتيجيتان اثنتان من استراتيجيات التحميل التي يشحنها PDFium Component إلى جانب تحميل الملف، والدفق، والمؤشر الخام. سطح الواجهة البرمجية الكامل، والترخيص، ودعم إصدارات Delphi وC++Builder موثَّق على صفحة منتج PDFium Component