حين يصبح جدول الإحالة المرجعية في PDF غير صالح للاستخدام، فإن الحل هو تجاهله كليًا وإعادة بنائه من جسم الملف. تنجز PDFlibPas Delphi PDF Library ذلك بماسح رموز أحادي المرور يسجّل كل ترويسة كائن غير مباشر حقيقية يراها، ثم يستعيد قاموس trailer ويسلّم الجدول المُعاد بناؤه إلى المُحمّل العادي
ما الذي ينكسر أولًا حين يتلف ملف PDF
جدول الإحالة المرجعية هو أهش جزء في PDF، لأنه الجزء الوحيد الذي يخزّن إزاحات بايت مطلقة. يعرّف ISO 32000-1 §7.5.4 تلك المدخلات كإزاحات من عشرة أرقام من بداية الملف، ويضع §7.5.5 الكلمة المفتاحية startxref قرب النهاية مشيرة إلى الجدول نفسه. كل رقم من تلك الأرقام يُبطَل بأي تعديل يُزحزح البايتات. جلسة FTP عملت بنمط نصي وحوّلت CRLF، أو تنزيل مُقطوع، أو قطاع تلف على قرص مشترك، أو أداة دفعية ألحقت بيانات دون كتابة تحديث تزايدي صحيح: كل هذه الحالات تترك بيانات الكائنات قابلة للقراءة تمامًا بينما يشير الفهرس إلى بيانات فاسدة
لهذا فإن رسالة "الملف تالف ويجري إصلاحه" شائعة إلى هذا الحد. البايتات ما تزال موجودة في الغالب الأعم. المفقود هو الخريطة. لذا فإن إعادة البناء ليست استعادة جنائية لبيانات مفقودة، بل إعادة بناء لفهرس يمكن اشتقاقه من الجسم، وتنجح في أحيان أكثر بكثير مما يتوقع المستخدمون لأن المحتوى الباهظ، من أشجار صفحات وخطوط وصور، يبقى دون مساس
لماذا يجد البحث عن N 0 obj تطابقات زائفة؟
إعادة البناء الساذجة تبحث في البايتات الخام عن نمط "عدد صحيح، عدد صحيح، obj" وتسجّل كل إصابة. فتجد أكثر مما ينبغي. صيغة PDF حاوية، وثلاث مناطق من الملف معتمة على قواعد الكائنات: التعليقات (§7.2)، والسلاسل النصية (§7.3.4)، وبيانات التدفقات (§7.3.8). أي منها قد يحتوي بايتات تُقرأ تمامًا كترويسة كائن، ولا شيء منها ترويسة كائن فعلًا. تعليق توضيحي داخل سلسلة نصية حرفية، أو تعليق تصحيح متبقٍ، أو ميغابايتان من مخرجات Flate أو DCT، كلها ستنتج بسعادة شيئًا يشبه 99 0 obj
const
Trap: AnsiString =
'4 0 obj'#10 +
'(a caption that mentions 88 0 obj)'#10 + // literal string, not an object
'endobj'#10 +
'% 77 0 obj left over from a debug dump'#10 + // comment, not an object
'5 0 obj'#10 +
'<< /Length 2097152 >>'#10 +
'stream'#10 +
{ two MiB of compressed bytes that contain the byte sequence
99 0 obj and, further along, a complete endstream }
'endstream'#10 +
'endobj'#10;
كل مدخل زائف يكلّف مرتين. فهو يلوّث الجدول المُعاد بناؤه برقم كائن غير موجود، وقد يحجب كائنًا حقيقيًا بالرقم نفسه يظهر لاحقًا في الملف. لذلك لا تقوم PDFlibPas بمطابقة أنماط على الإطلاق. إنها تُرمّز tokenise، أي أنها تعرف دومًا ما إذا كانت البايتات تحت المؤشر رمزًا code أم حمولة payload، وتتخطى الحمولة دون تفسيرها قط
آلة حالات أحادية المرور فوق كتل 64 كيبيبايت
تفحص PDFlibPas الملف بأكمله مرة واحدة بالضبط، في كتل من 64 كيبيبايت، بآلة حالات مبنية على قواعد الرموز في ISO 32000-1 §7.2 وصياغة الكائن غير المباشر في §7.3.10. ينتهي الرمز عند مسافة بيضاء أو أحد محارف الفاصل، ولا تُسجَّل ترويسة كائن إلا حين تُرى سلسلة كاملة من رقم كائن موجب ورقم جيل غير سالب وكلمة obj مفتاحية مجردة. الإزاحة المسجَّلة هي بداية رمز رقم الكائن، وهي ما يجب أن يشير إليه مدخل الإحالة المرجعية، لا موضع الكلمة المفتاحية obj
function RebuildIsWhiteSpace(Value: Byte): Boolean;
begin
Result := (Value = 0) or (Value = 9) or (Value = 10) or
(Value = 12) or (Value = 13) or (Value = 32);
end;
function RebuildIsDelimiter(Value: Byte): Boolean;
begin
Result := (Value = Ord('(')) or (Value = Ord(')')) or
(Value = Ord('<')) or (Value = Ord('>')) or
(Value = Ord('[')) or (Value = Ord(']')) or
(Value = Ord('{')) or (Value = Ord('}')) or
(Value = Ord('/')) or (Value = Ord('%'));
end;
التفصيل المهم هو أن حالة الرمز وحالة السلسلة تنجوان عبر حدود الكتلة. تُتعرَّف الترويسة التي تمتد عبر خط 65536 بايت رغم ذلك، لأن الرمز الجزئي وزوج الأعداد الصحيحة المعلَّق وأعلام داخل-السلسلة كلها تنتقل إلى الكتلة التالية. المخازن المؤقتة ثابتة: 64 كيبيبايت للفحص، و32 بايتًا لأطول رمز يمكن أن يهم على الإطلاق، والمصفوفات الوحيدة التي تكبر مع الملف هي قوائم رقم الكائن ورقم الجيل والإزاحة من 64 بت، وهي متناسبة مع عدد الكائنات الفعلي لا مع حجم الملف. عمليًا يصدر الفحص قراءات متسلسلة وحد أقصى قفزتين seek صريحتين على المستند بأكمله، وهو ما يجعله عمليًا على المدخلات التي تتجاوز مئات الميغابايتات المشروحة في المقال عن الدمج والتقسيم بالوصول المباشر
لماذا لا يمكن الوثوق بأن ينتهي التدفق عند endstream؟
لأن بيانات التدفق بايتات عشوائية، والبايتات العشوائية قد تهجّي endstream عرضًا. التدفق الذي يبدأ بعد الكلمة المفتاحية stream يجب تخطيه كبيانات معتمة حتى ينتهي فعلًا، لكن الظهور الأول للكلمة المفتاحية الختامية ليس إلا مرشحًا. تحل PDFlibPas هذا باشتراط تأكيد إضافي: لا يُقبَل رمز endstream نهاية حقيقية للتدفق إلا حين يكون الرمز التالي غير الفارغ من مسافات بيضاء هو endobj قائمًا بذاته، وهو التسلسل الذي يفرضه §7.3.8 حول كائن تدفق. إصابة عرضية داخل بيانات مضغوطة نادرًا ما تملك ذلك التتابع، فيبقى الماسح داخل التدفق ويواصل. قاعدتان أصغر تهمّان بالقدر نفسه. الكلمة المفتاحية stream تدخل حالة التدفق فقط حين تكون كلمة مفتاحية مجردة، فكائن اسم مثل /stream داخل قاموس لا يُطلقها أبدًا. ورمز obj أو trailer لا يُعتد به إلا حين لا يتجاوز الرمز سقف 32 بايتًا ولا يبدأ بشرطة مائلة. بدون هذين الحارسين كان قاموس موارد بأسماء مفاتيح خاطئة كافيًا لإخراج الفحص عن مساره، وهو بالضبط صنف المدخلات العدائية المشروح في ملاحظات تحليل ملفات PDF غير الموثوقة بأمان
إيجاد النهاية الحقيقية لقاموس trailer
استعادة الكائنات نصف المهمة فقط، لأن المُحمّل ما يزال يحتاج trailer ليجد /Root. تتذكر PDFlibPas آخر 64 موضعًا لكلمة trailer المفتاحية عُثر عليها أثناء الفحص وتتحقق منها بترتيب عكسي، الأحدث أولًا، بحيث يفوز أحدث trailer صالح للاستخدام، وأي كلمة مفتاحية شاردة لا يتبعها قاموس تفشل ببساطة في التحقق وتنتقل إلى المرشح السابق. يُقرأ كل مرشح بسقف قدره 1 ميبيبايت، وتُحدَّد نهاية القاموس بتتبع عمق << و>> المتداخل إلى جانب هروبات السلاسل الحرفية والسلاسل السداسية عشرية والتعليقات
// A naive reader that stops at the first '>>' truncates this trailer,
// and a fixed 2048-byte window can cut it in half on a large one
'trailer'#10 +
'<< /Size 5 /Root 1 0 R' +
' /Custom << /Text (value >> preserved) >> >>'#10
تتبع العمق ليس مسألة أكاديمية. trailer مبتور يفقد /Encrypt يحوّل مستندًا مشفَّرًا قابلًا للاستعادة إلى مستند يتعذر فتحه، وفقدان /Info أو قاموس فرعي مخصَّص يُسقط بصمت بيانات وصفية قد يعتمد عليها نظام لاحق. إن كان الملف مشفَّرًا، فإن trailer المُستعاد هو ما يتيح تشغيل مسار بيانات الاعتماد العادي، وسلوكيات إعادة المحاولة هي نفسها الموصوفة في المقال عن تحميل المستندات المشفَّرة
ما الذي لا تستطيع إعادة البناء استرجاعه
إعادة البناء بذل أفضل جهد، والصراحة بشأن حدودها جزء من إصدارها. ثلاث حالات تفشل تمامًا. الكائنات المعبّأة داخل تدفقات كائنات (§7.5.7) غير مرئية فرادى لفحص البايتات، فإن نجت حاوية لكن تدفق الإحالة المرجعية الخاص بها (§7.5.8) لم ينجُ، فإن الكائنات التي تحملها لا تُفهرس بإعادة البناء. الملف الذي تلف جسمه فعلًا، لا الذي أخطأ فهرسته فحسب، سينتج ترويسات لم تعد محتوياتها قابلة للتحليل. والملف الذي لا كلمة trailer مفتاحية قابلة للاستعادة فيه ولا فهرس catalog قابل للقراءة ليس له ما يُرسي عليه شجرة مستند، بصرف النظر عن عدد ترويسات الكائنات التي عُثر عليها
أرقام الكائنات المكررة هي الحالة الوسطى المثيرة للاهتمام. الملف المحدَّث تزايديًا يحتوي شرعيًا عدة أجيال من رقم الكائن نفسه، وسلسلة الإحالة المرجعية الناجية هي السجل الوحيد لمعرفة أيها كان الحالي. إعادة البناء لا تملك تلك السلسلة، فتسجّل كل ترويسة تراها بترتيب الملف وتحلّها بحسب رقم الكائن لاحقًا. عادة تفوز المراجعة الأحدث، وهذا صحيح عادة، لكن مستندًا حُدِّث ثم أُرجع جزئيًا قد يعود مختلفًا بدقة عمّا وصفه xref الأصلي. الملفات المُخطّطة linearised تحمل التحذير نفسه من الجهة المقابلة: تخطيط الصفحة الأولى وجداول التلميحات تفقد معناها بمجرد إعادة توليد الفهرس، فينبغي معاملة الملف المُصلَح كمستند عادي غير مُخطَّط
var
Pdf: TPDFlib;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile('truncated-invoice.pdf', '') = 1 then
begin
if Pdf.GetDocumentRepaired = 1 then
LogWarning('xref was unusable; the table was reconstructed');
if Pdf.PageCount > 0 then
Pdf.SaveToFile('recovered-invoice.pdf'); // writes a clean xref
end;
finally
Pdf.Free;
end;
end;
الرجوع إلى هذا المسار تلقائي: تشغّل PDFlibPas الفحص الخام كلما تعذّرت قراءة سلسلة الإحالة المرجعية، وكذلك حين يدّعي كل مدخل قيد الاستخدام إزاحة صفرية، وهي بصمة جدول كُتب لكنه لم يُملأ قط. يُعيد GetDocumentRepaired القيمة 1 حين يعمل ذلك المسار، ويستحق التسجيل لا التجاهل، لأن المستند الذي حُمِّل عبر إعادة البناء ينبغي إعادة حفظه إلى ملف نظيف لا تركه في خط معالجة كأن شيئًا لم يحدث. حفظه يكتب جدول إحالة مرجعية جديدًا ومتّسقًا، وهو أرخص إصلاح ممكن لكل مستهلك لاحق
مسار إعادة البناء، وعلامة GetDocumentRepaired، والمُحمِّل المتدفق المعروضان هنا جزء من PDFlibPas Delphi PDF Library، إلى جانب واجهات التحليل والعرض والتوقيع المشروحة في مواضع أخرى من هذه المدونة