يكشف مكوّن PDFium عن TPdfParserResourceBudget، سجلّة واحدة تقيّد عمق التكرار وعدد الرموز وعدد الكائنات والبايتات المفكوكة ونسبة التوسع والتخصيص وموعدًا نهائيًا بوقت الحائط لكل محلل PDF في المكتبة. يضبطها ConfigurePdfParserResourceBudget ذريًا على مستوى العملية، وكل عملية تحليل تنسخ لقطة فلا يمكن لعتباتها أن تتحول في منتصف العملية
ستة محللين، ستة سقوف خاصة
الموقف الذي فرض هذا كان عاديًا وربما مألوفًا. لاجتياز xref المتفرق سقف كائناته الخاص. ولمفتش التدفقات سقف بايتات مفكوكة خاص به. XMP له سقف، وXFDF وFDF لهما سقف آخر، وتطبيع المحتوى له ثالث، وافتراضيات ما قبل الطيران لها رابع. كل رقم من تلك الأرقام كان دفاعيًا بمفرده، لكن لا يمكن الوصول إلى أي منها من الخارج، فالمتصل الذي يريد أن يقول «مسار الاستقبال هذا يتعامل مع رفع مجهول، لا يجوز لأي شيء هنا تخصيص أكثر من 64 MiB أو العمل أطول من أربع ثوانٍ» لم يجد مكانًا يقول فيه ذلك. المشكلة الثانية كانت أسوأ لأنها صامتة: حيثما عاش حد في متغير عام قابل للتغيير، كان تحليل يبدأ في ظل سياسة قد ينتهي في ظل أخرى. خيط يستدعي ضابطًا بينما خيط آخر في منتصف تدفق كائنات، فيكون النصف الأول من ذلك الاجتياز مقيدًا بالسقف القديم بينما النصف الثاني مقيد بالجديد. لا شيء ينهار، ولا شيء يُسجل، وبعد ذلك لا تستطيع أن تقول تحت أي حدود عملت العملية فعلًا، وهي بالضبط الخاصية التي كنت تريد الحدود من أجلها
ما الذي تقيّده TPdfParserResourceBudget فعلًا؟
سبعة أبعاد، اختيرت لأن كل واحد منها يلتقط فئة مدخلات تفلت من البقية. MaxRecursionDepth افتراضه 1024 ويوقف القواميس والمصفوفات المتداخلة عن تحويل صياغة كائنات ISO 32000-1 §7.3 إلى تجاوز مكدس. MaxTokens افتراضه 10,000,000 ويقيّد عمل التحليل المعجمي بصرف النظر عن حجم الملف. MaxObjects افتراضه 4,000,000 ويغطي أعداد الأقسام الفرعية للفهرس المتقاطع وقيم /N في تدفقات الكائنات واجتيازات المخططات. MaxDecodedBytes افتراضه 256 MiB وMaxAllocationBytes افتراضه 512 MiB، فيمكن رفض الملف قبل التخصيص لا بعده. MaxExpansionRatio افتراضه 1000.0 وهو الذي يستهين به الناس: مرشحات التدفق في ISO 32000-1 §7.4 ضغط للأغراض العامة، فبضعة كيلوبايتات من بيانات Flate يمكنها شرعيًا أن تتوسع إلى مئات الميغابايتات، وسقف النسبة هو الدفاع المعياري ضد قنبلة فك الضغط. وهو عمودي على سقف البايتات المطلق، لأن تدفق 200 KiB يريد أن يصبح 200 MiB تحت سقف 256 MiB وما يزال سخيفًا. DeadlineMilliseconds افتراضه 0، أي بلا موعد نهائي، فذلك البعد اختياري صرامة. الافتراضات كمجموعة سخية عمدًا، بمقاس يحفظ مستندات الأعمال العادية من التعثر بأي شيء لا كملف تعريف مقسى؛ إذا وصل مدخلك من نموذج رفع عام فأنت تريد شيئًا أقرب إلى الأرقام أدناه، إلى جانب القواعد الموصوفة في تقسية لوحة معاينة PDF في دلفي
uses
FPdfPdfCommon;
procedure ApplyUntrustedIntakeBudget;
var
Budget: TPdfParserResourceBudget;
begin
Budget := TPdfParserResourceBudget.Default;
// الافتراضات المبقاة: MaxRecursionDepth 1024 وMaxTokens 10000000
// وMaxObjects 4000000
Budget.MaxDecodedBytes := Int64(32) * 1024 * 1024; // الافتراضي 256 MiB
Budget.MaxAllocationBytes := Int64(64) * 1024 * 1024; // الافتراضي 512 MiB
Budget.MaxExpansionRatio := 50.0; // الافتراضي 1000.0
Budget.DeadlineMilliseconds := 4000; // الافتراضي 0 = معطل
ConfigurePdfParserResourceBudget(Budget);
end;
لماذا تؤخذ لقطة بدلًا من قراءة المتغير العام؟
لأن حدًا يمكن أن يتغير بينما يُفرض ليس حدًا. هذه هي النقطة التصميمية الجوهرية في الميزة كلها، وهي سطر انضباط واحد عند كل نقطة دخول: اقرأ السياسة مرة واحدة في بداية العملية في TPdfParserResourceBudget محلي، ثم اجعل كل نقطة تفتيش تقارن ضد تلك النسخة المحلية. تأخذ GetPdfParserResourceBudget القسم الحرج وتعيد نسخة قيمة من السجلّة، وبعد ذلك لا تلمس عملية التحليل الحالة العامة القابلة للتغيير مجددًا. النتيجة أن استدعاء ConfigurePdfParserResourceBudget من خيط آخر يؤثر في العملية التالية لا في الجارية. تحليلات متزامنة تعمل بسياسات مختلفة دون تداخل، والعملية التي فشلت بسبب حد موارد فشلت ضد مجموعة عتبات متماسكة واحدة، ولا يوجد تنازع على الأقفال في المسار الساخن لأن القفل يُؤخذ مرة لكل تحليل لا مرة لكل رمز
type
TCustomScanState = record
Budget: TPdfParserResourceBudget;
StartTick: Cardinal;
TokenCount: Int64;
Depth: Integer;
end;
procedure BeginScan(var S: TCustomScanState);
begin
S.Budget := GetPdfParserResourceBudget; // لقطة تؤخذ مرة واحدة في البداية
S.StartTick := PdfParserStartTick;
S.TokenCount := 0;
S.Depth := 0;
end;
function ScanCheckpoint(var S: TCustomScanState): Boolean;
begin
Result := False;
if PdfParserDeadlineReached(S.StartTick, S.Budget) then
Exit;
Inc(S.TokenCount);
if S.TokenCount > S.Budget.MaxTokens then
Exit;
if S.Depth > S.Budget.MaxRecursionDepth then
Exit;
Result := True;
end;
إذا كتبت محللك الخاص فوق المكتبة، فانسخ هذا الشكل بدلًا من استدعاء GetPdfParserResourceBudget داخل حلقة الرموز. استدعاؤها لكل رمز صحيح لكنه بلا فائدة: تدفع ثمن قسم حرج في كل تكرار وتعيد إدخال الانجراف في منتصف العملية الذي كانت اللقطة موجودة لمنعه
لماذا لا يوقف سقف البايتات ملفًا مرضيًا؟
لأن الحجم والتكلفة في PDF مرتبطان ارتباطًا فضفاضًا فقط. ملف 400 KiB بتيار فهرس متقاطع وفق ISO 32000-1 §7.5.8 تصف مصفوفتاه /Index و/W عدد إدخالات معقولًا ظاهريًا لكنه عدائي، أو سلسلة تدفقات كائنات تعيد زيارة نفس الإزاحات، أو تدفق محتوى من عوامل تشغيل كل واحد منها يكلف أكثر مما يبدو، سيجلس مريحًا تحت كل سقف بايتات وكائنات تضبطه وما يزال يشغل خيط عامل لدقائق. عدادات البايتات تقيّد الذاكرة؛ لا تقيّد الوقت. هذا ما وُجدت له PdfParserStartTick وPdfParserDeadlineReached. تلتقط PdfParserStartTick قيمة tick في بداية العملية وتقارن PdfParserDeadlineReached الوقت المنقضي بـ DeadlineMilliseconds، معيدة False كلما كان الموعد النهائي 0. الطرح يجري بحساب غير موقّع، فلا ينتج عن انقلاب عداد tick مهلة زائفة. تحليل الفهرس المتقاطع وفك ترميز تدفقات الكائنات وتطبيع رموز المحتوى وقارئا FDF وXFDF كلها تفحصه عند رأس حلقاتها، فيتوقف تحليل تجاوز الميزانية عند نقطة تفتيش بدلًا من قتله من الخارج
تحفظان صادقان. إنه وقت الحائط لا وقت CPU، فجهاز يقوم بالمبادلة أو خادم بناء تحت الحمل يمكن أن يضرب موعدًا نهائيًا كان جهاز هادئ سيجتازه، وهو ما يرجح ضبطه أعلى بكثير من أسوأ حالة لاحظتها لا قريبًا من متوسطك. وهو معطل افتراضيًا، لأن موعدًا نهائيًا يطلق على مستند كبير شرعي أثناء معالجة دفعات نهاية الشهر فشل أسوأ من الذي يمنعه. اختر تفعيله لكل مسار استقبال لا عالميًا، واقرنه بالفحوص البنيوية في التحقق من تدفقات الفهرس المتقاطع والكائنات
الميزانية سقف، لا بديل عن حدود الصيغة
يحتفظ كل محلل بثوابته الخاصة بالصيغة ويطبق الاثنين معًا. XFDF أوضح مثال: يفرض قارئ الحقول XfdfMaxTopLevelFields وثابت MaxFieldNodes قيمته 100000 إلى جانب قيمة اللقطة، وأي منهما يمكن أن يوقف التحليل
const
XfdfMaxTopLevelFields = 100000; // خاص بالصيغة، يطبق دائمًا
...
// السقفان حيان معًا؛ من هو أدنى يفوز
if (FieldCount >= XfdfMaxTopLevelFields) or
(FieldCount >= Budget.MaxObjects) then
Exit(False);
هذا مقصود ويستحق الاستيعاب قبل أن تبحث عن مقبض غير موجود. ميزانية عالمية من 4,000,000 كائن لا تمنح ملف XFDF 4,000,000 حقل، لأن 100000 حقل خارج بالفعل وبعيدًا عن أي شيء ينتجه تصدير تعليقات حقيقي، ورفع سقف عالمي لا يجب أن يوسع بصمت فحص سلامة خاصًا بصيغة. تطبيع المحتوى يعمل بالطريقة نفسها من الاتجاه المعاكس: يبدأ TPdfContentNormalizeOptions.Default من حدوده الخاصة لكل صفحة ثم يقص كلًا منها إلى اللقطة، فـ MaxTokensPerPage لا يمكن أن يُخفض إلا بالميزانية ولا يُرفع أبدًا. السياسة العامة تشد؛ ولا ترتخي أبدًا
الإعدادات السيئة تفشل وقت ضبطها
يتحقق ConfigurePdfParserResourceBudget قبل أن يسند. قيمة غير موجبة في أي من الأبعاد الرقمية الستة تطلق استثناء فورًا، وMaxDecodedBytes فوق High(Integer) تُرفض لأنها تتجاوز حدود المنصة لا لأنها تُقتطع بصمت في مكان عميق في مفكك الترميز. هذا أهم مما يبدو: ميزانية مبنية من ملف إعدادات بمفتاح مفقود تعطيك أصفارًا، وسقف كائنات صفري مقبول عند الإقلاع يتحول إلى فشل تحليل محيّر في أول مستند بعد ثلاث ساعات، وعندها لا يشك أحد في الإعدادات. العناية نفسها تنطبق على أي كود يشد الميزانية مؤقتًا، بما فيه الاختبارات. السياسة على مستوى العملية، فترك موعد نهائي من 4 ثوانٍ مثبتًا بعد انتهاء وحدة إعداد يسمم كل ما يليها؛ احفظ واستعد حول التغيير
var
Saved: TPdfParserResourceBudget;
begin
Saved := GetPdfParserResourceBudget;
try
ApplyUntrustedIntakeBudget;
ProcessInboundDocument(FileName);
finally
ConfigurePdfParserResourceBudget(Saved);
end;
end;
ResetPdfParserResourceBudget النسخة الأكثر خشونة، إذ تستعيد TPdfParserResourceBudget.Default بالكامل. استخدمها في تنظيف الاختبارات حيث تريد حالة معلومة، واستخدم الحفظ والاستعادة في كود الإنتاج، حيث ما ضبطه التطبيق المضيف عند الإقلاع هو الحالة التي يجب أن تعود إليها
حيث تتوقف الميزانية عن المساعدة
إنها سياسة واحدة على مستوى العملية، وهذا حدها الرئيسي. لا يمكنك منح خيط عامل ملف تعريف صارمًا وآخر متساهلًا، لأن هناك سجلّة واحدة خلف قسم حرج واحد؛ إذا كنت تحتاج فعلًا حدودًا لكل مستأجر، فالشكل الذي يعمل هو تسلسل كل فئة سياسة على عملية مستقلة، أو تأطير العمل بالحفظ والاستعادة مع قبول أن الإطار آمن فقط عندما لا يحلل أي شيء آخر بالتوازي. كما أنها تقيّد التحليل لا العرض: ذاكرة التنقيط ونمو مخبأ الخطوط وفك ترميز الصور خارج هذه الأبعاد السبعة وتحتاج ضوابطها الخاصة. والميزانية تخبرك أن عملية كانت مكلفة جدًا، لا لماذا أبدًا. السقف الذي انطلق هو العَرَض، ومصفوفة /Index المشوهة أو سلسلة المرشحات الهاربة خلفه ما يزال يتعين تشخيصها يدويًا، ولهذا يستحق مسار فك الترميز الموصوف في فك ترميز تدفقات الكائنات والمُنبئ الفهم قبل أن تبدأ بتحريك الأرقام. عامل الميزانية كالشيء الذي يمنع مستندًا سيئًا من إسقاط العملية معه، وأبقِ تسجيلك جيدًا بما يكفي لتكتشف ما كان يفعله المستند
سطح واجهة المحلل الكامل وسجلّة الميزانية وبناء من الكود المصدري لدلفي وC++Builder وLazarus كلها على صفحة المنتج: مكوّن PDFium