تخطَّ إلى المحتوى

التحديثات التزايدية وسبب أهميتها

Evidence: Standard-backed

عندما يتغير ملف ⁨PDF⁩ بعد كتابته، فالطريقة الآمنة لحفظه ليست إعادة كتابة الملف. بدلاً من ذلك، تُلحِق الكائنات المتغيرة وقسم مراجع تقاطعية جديداً في النهاية، وتترك كل بايت أصلي في موضعه تماماً. توضّح هذه الصفحة آلية ذلك، وسبب قدرته على إبقاء التوقيع الرقمي صالحاً بعد تحرير لاحق.

يحمي التوقيع نطاقاً من البايتات. لو أعاد حفظ تغيير في كلمة واحدة كتابة الملف، لتحرّكت كل إزاحة بايت. عندها لن يعود النطاق الموقَّع يصف المحتوى نفسه، وسيفسد التوقيع حتى لو لم يُمَس المحتوى الموقَّع نفسه.

توجد التحديثات التزايدية لمنع ذلك. تبقى البايتات الأصلية، بما فيها البايتات التي يغطيها التوقيع، في موضعها. يستطيع المراجِع أخذ مستند وُقِّع ثم حُرِّر، والتحقق من التوقيع الأول مقابل المراجعة الأصلية. بذلك يرى المراجِع بدقة ما جرى توقيعه، ويرى بصورة منفصلة ما تغيّر بعد ذلك. إذا أخطأت في هذا فإما أن تُبطِل توقيعات سليمة، أو، وهذا أسوأ، تفقد القدرة على إثبات ما شهد به التوقيع فعلاً.

  • يقوم التحديث التزايدي بـالإلحاق: كائنات جديدة ومتغيرة، ثم قسم مراجع تقاطعية جديد، ثم خاتمة جديدة، كلها في نهاية الملف.
  • يبقى محتوى الملف الأصلي سليماً — لا يُحرَّر في مكانه.
  • تحمل الخاتمة الجديدة مدخل /Prev: إزاحة البايت لقسم المراجع التقاطعية السابق. تشكّل الأقسام سلسلة رجعية.
  • يبني القارئ فهرسه بالسير عبر تلك السلسلة من الأحدث أولاً. بالنسبة لأي رقم كائن، يفوز المدخل الأحدث.
  • لأنه لم يُكتَب فوق أي شيء، يظل نطاق البايتات الذي غطّاه توقيع سابق متطابقاً بايتاً ببايت — لذلك يظل التوقيع قابلاً للتحقق، ويمكنك استرجاع المستند تماماً كما وُقِّع.

يكتب ⁨NextPDF⁩ المستند الأساس كما هو موصوف في الصفحة السابقة، ثم يتيح العناصر الثلاثة التي يحتاجها التحديث التزايدي.

بعد build()، يحتفظ الكاتب (src/Writer/PdfWriter.php) بما يلي:

  • المخزن المؤقت للمخرجات، القابل للاسترجاع عبر getBuffer()، كي يمكن إلحاق تحديث بنهاية البايتات الموجودة تماماً؛
  • إزاحة البايت لآخر قسم مراجع تقاطعية، عبر getLastXrefOffset()، التي تصبح قيمة /Prev للقسم الجديد؛
  • مدخلات قاموس الفهرس، عبر getCatalogEntries()، كي لا يفقد التحديث أي مفاتيح سابقة عند اضطراره إلى إعادة إصدار الفهرس (على سبيل المثال لإرفاق مرجع توقيع).

تخصص المراجعة المُلحَقة أرقام كائنات جديدة (أو تعيد استخدام الأرقام القائمة للكائنات التي تستبدلها) مقابل ObjectRegistry نفسه، فيبقى ترقيم الكائنات متسقاً عبر المراجعات. يسرد قسم المراجع التقاطعية الجديد الكائنات التي مستها هذه المراجعة فقط. تكرر الخاتمة الجديدة مدخلات الخاتمة السابقة وتضيف /Prev، مشيرةً رجوعاً إلى القسم السابق. هذه السلسلة هي ما يتبعه القارئ.

أوضح موضع تظهر فيه أهمية ذلك هو التوقيع. يحسب مكوّن ByteRangeCalculator في ⁨NextPDF⁩ (src/Security/Signature/ByteRangeCalculator.php) مصفوفة /ByteRange بوصفها مقطعين: كل ما قبل قيمة التوقيع، وكل ما بعدها — فيغطي التوقيع المراجعة بأكملها باستثناء بايتاته الخاصة. ولأن أي تحرير لاحق يُلحَق بدلاً من الكتابة فوق تلك البايتات، فإن ذلك النطاق لا يتحرك أبداً.

  1. Write base revision Header, body, xref section, trailer — the original bytes.
  2. Sign A /ByteRange digest covers the whole revision except the signature value itself.
  3. Edit and save Changed objects + a new xref section are appended; originals are untouched.
  4. New trailer chains back The appended trailer carries /Prev = offset of the previous xref section.
  5. Verify The first signature still covers the same unchanged bytes; the chain shows what came after.
كيف يبقى ملف PDF موقَّع جرى تحريره لاحقاً قابلاً للتحقق: كل عملية حفظ تُلحِق، فلا يُكتَب فوق البايتات الموقَّعة أصلاً أبداً، وتسجل سلسلة الخواتم السجل التاريخي.

قاعدة الإلحاق فقط قاعدة معيارية. Spec: ISO 32000-2, §7.5.6 ينص على أن محتويات ملف PDF يمكن تحديثها تزايدياً دون إعادة كتابة الملف بأكمله، وأنه عند فعل ذلك يجب إلحاق التغييرات بنهاية الملف، مع ترك المحتويات الأصلية سليمة. Evidence: Standard-backed

يحدد البند نفسه آلية العمل. يحتوي قسم المراجع التقاطعية في التحديث التزايدي على مدخلات للكائنات التي تغيّرت أو استُبدلت أو حُذفت فقط. تُترك الكائنات المحذوفة في الملف، لكنها تُوسَم بأنها محذوفة عبر مدخلاتها التقاطعية. يجب أن تحتوي الخاتمة المضافة على مدخل /Prev يعطي موقع قسم المراجع التقاطعية السابق. يحمل مدخل التحديث لكائن متغير إزاحة البايت للنسخة الجديدة، متجاوزاً الإزاحة القديمة. يبني القارئ معلومات مراجعه التقاطعية بحيث تكون النسخة الأحدث من كل كائن هي التي يجري الوصول إليها.

يُذكر أثر التوقيع مباشرةً في Spec: ISO 32000-2, §12.8.1 : يُحسب مُلخَّص نطاق بايتات على نطاق من الملف — عادةً الملف بأكمله، باستثناء قيمة التوقيع (مدخل /Contents). ثم يلاحظ المعيار أنه إذا عُدِّل مستند موقَّع وحُفظ بتحديث تزايدي، فإن البيانات المقابلة لنطاق بايتات التوقيع الأصلي تُحفظ، فإذا كان التوقيع صالحاً أمكن أن تُعاد حالة المستند وقت التوقيع إلى ما كانت عليه. الإلحاق فقط ليس ترفاً. إنه الخاصية التي يعتمد عليها نموذج التوقيع.

ملف ⁨PDF⁩ وُقِّع ثم حُرِّر، معروضاً على مستوى البنية. تنتهي المراجعة الأصلية عند %%EOF الخاص بها. تُلحَق المراجعة الثانية أسفلها.

%PDF-2.0
... original objects, including the signature dictionary ...
xref
0 8
... entries for the original revision ...
trailer
<< /Size 8 /Root 1 0 R >>
startxref
920
%%EOF
<-- end of revision 1: the signed bytes stop here
9 0 obj <-- revision 2, appended
<< /Type /Annot /Subtype /Text /Contents (added after signing) >>
endobj
xref
0 1
9 0 obj-entry...
8 9
0000001740 00000 n
trailer
<< /Size 10 /Root 1 0 R /Prev 920 >>
startxref
1980
%%EOF

يقرأ المُتحقِّق الخاتمة الأخيرة، ويرى /Prev 920، فتتكوّن لديه السلسلة بأكملها. يمكنه التحقق من التوقيع مقابل البايتات حتى أول %%EOF، وهي بايتات لم تتغير. ثم يمكنه أن يبلّغ بصورة منفصلة بأن المراجعة 2 أضافت تعليقاً توضيحياً. السجل التاريخي موجود في الملف. لم يُخفَ أي شيء بالكتابة فوقه.

المزلق هو الاعتقاد بأن “التحديث التزايدي يعني أن التغيير صغير، ومن ثَمّ فهو غير ضار.” الإلحاق يتعلق بـالحفاظ على البايتات، لا بالحجم. يمكن لتحديث تزايدي أن يضيف قدراً كبيراً من المحتوى. ما يجعله تحديثاً تزايدياً هو أنه لا يمس البايتات التي كانت موجودة بالفعل. والنتيجة العملية تفاجئ الناس أيضاً: أداة “تُحسِّن” أو “تُخطِّط خطياً” ملف ⁨PDF⁩ موقَّعاً بإعادة كتابته من الصفر ستنتج ملفاً أصغر وأنظف وتوقيعاً معطوباً، لأن نطاق البايتات الموقَّع لم يعد موجوداً. حفظ ملف ⁨PDF⁩ موقَّع وإعادة حفظه ليسا العملية نفسها.

يحمي الإلحاق فقط البايتات. لكنه لا يخبرك، بمفرده، بما إذا كانت التغييرات المُلحَقة مصرَّحاً بها. يمكن لمراجعة ثانية أن تضيف توقيعاً ثانياً بصورة مشروعة، أو أن تضيف محتوى لم يقصده الموقِّع الأول أبداً. تحديد أيهما حدث هو دور التحقق من التوقيع وسياسة كشف التعديل (⁨DocMDP⁩). الإلحاق هو الركيزة التي تجعل ذلك التحليل ممكناً، لا التحليل نفسه.

كما لا تتناول هذه الصفحة كيفية حساب نطاقَي بايتات التوقيع وربطهما، ولا ما يفحصه التحقق الكامل. تلك مواضيع منفصلة. والضمان هنا يتعلق بالملفات التي يكتبها ويحدِّثها كاتب مطابق: فالملف الذي كانت مراجعاته السابقة مشوَّهة التكوين أصلاً لا يصبح صحيح التكوين بمجرد الإلحاق به.

كيف أعرف عدد المراجعات في ملف ⁨PDF⁩؟ عُدّ علامات %%EOF واتبع سلسلة /Prev من الخاتمة الأخيرة. كل قسم مراجع تقاطعية تصل إليه يمثل مراجعة محفوظة واحدة.

هل يؤدي حذف كائن إلى إزالته من الملف؟ لا. يوسم التحديث التزايدي الكائن بأنه محذوف في مدخله التقاطعي، لكن بايتات الكائن تبقى في المراجعات السابقة. “محذوف” تعني “غير مُشار إليه من المراجعة الحالية”، لا “مُمحى.”

هل يمكن لتحديث تزايدي أن يغيّر إصدار ⁨PDF⁩؟ نعم، بتعيين مدخل /Version في الفهرس ضمن المراجعة المُلحَقة. تبقى الترويسة كما كُتبت. يكون لمدخل /Version في الفهرس الأسبقية عندما يسمّي إصداراً أحدث.

  • التحديث التزايدي — حفظ تغيير بإلحاق الكائنات المتغيرة وقسم مراجع تقاطعية جديد وخاتمة جديدة بنهاية الملف، دون تغيير البايتات الموجودة.
  • /Prev — مدخل الخاتمة (أو دفق المراجع التقاطعية) الذي يحمل إزاحة البايت لقسم المراجع التقاطعية السابق. يربط المراجعات في سلسلة رجعية.
  • المراجعة — حالة الملف التي يلتقطها قسم مراجع تقاطعية واحد وخاتمته. الملف الذي يحوي ⁨N⁩ أقسام مراجع تقاطعية له ⁨N⁩ مراجعات.
  • /ByteRange — المصفوفة في قاموس التوقيع التي تعطي مقطعَي البايتات اللذين يغطيهما مُلخَّص التوقيع (كل شيء باستثناء قيمة التوقيع نفسها).
  • نطاق البايتات الموقَّع — البايتات الدقيقة التي حُسب عليها مُلخَّص التوقيع. توجد التحديثات التزايدية كي لا تُنقَل هذه البايتات ولا يُكتَب فوقها أبداً.