مقاله فنی

بازسازی جدول Xref خراب PDF: اسکن بازیابی Delphi

وقتی یک جدول مرجع متقابل PDF غیرقابل‌استفاده است، رفع مشکل این است که کاملاً نادیده‌اش بگیرید و آن را از بدنه فایل بازسازی کنید. کتابخانه PDF Library for Delphi Delphi PDF این کار را با یک اسکنر توکن تک‌گذر انجام می‌دهد که هر سربرگ آبجکت غیرمستقیم واقعی که می‌بیند را ثبت می‌کند، سپس دیکشنری trailer را بازیابی می‌کند و جدول بازساخته را به بارگذار معمولی می‌سپارد

خط لوله ترمیم PDF Library for Delphi هنگام آسیب جدول ارجاع متقابل: بارگذار به اسکن بازیابی تک‌گذره برمی‌گردد و ذخیره مجدد تمیز یک xref تازه تولید می‌کند
آنچه شکست خورده ایندکس است نه بایت‌ها: یک اسکن جایگزین، آفست‌های اشیاء و trailer قابل استفاده را بازسازی می‌کند، لودر عادی ادامه می‌دهد، و ذخیره مجدد یک xref تمیز صادر می‌کند

وقتی یک 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 +   // رشته تحت‌اللفظی است، نه یک آبجکت
    'endobj'#10 +
    '% 77 0 obj left over from a debug dump'#10 +  // کامنت است، نه یک آبجکت
    '5 0 obj'#10 +
    '<< /Length 2097152 >>'#10 +
    'stream'#10 +
    { دو MiB بایت فشرده که دنباله بایتی
      99 0 obj و کمی جلوتر یک endstream کامل را در خود دارند }
    'endstream'#10 +
    'endobj'#10;

هر مدخل اشتباه دوبار هزینه می‌کند. جدول بازساخته را با یک شماره آبجکت که وجود ندارد آلوده می‌کند، و می‌تواند یک آبجکت واقعی با همان شماره که بعداً در فایل ظاهر می‌شود را سایه بیندازد. PDF Library for Delphi بنابراین اصلاً الگو-تطبیقی نمی‌کند. توکن‌سازی می‌کند، که یعنی همیشه می‌داند آیا بایت‌های زیر نشانگر کد هستند یا بار محتوایی، و بار محتوایی بدون اینکه هرگز تفسیر شود رد می‌شود

یک ماشین حالت تک‌گذر روی بلوک‌های ۶۴ کیلوبایتی

PDF Library for Delphi کل فایل را دقیقاً یک‌بار، در بلوک‌های ۶۴ کیلوبایتی، با یک ماشین حالت ساخته‌شده روی قواعد توکن ISO 32000-1 §7.2 و نحو آبجکت غیرمستقیم §7.3.10 اسکن می‌کند. یک توکن در فاصله سفید یا یکی از کاراکترهای جداکننده پایان می‌یابد، و یک سربرگ آبجکت فقط زمانی ثبت می‌شود که یک دنباله کامل از یک شماره آبجکت مثبت، یک شماره نسل غیرمنفی، و یک کلمه کلیدی خالص obj دیده شود. آفست ثبت‌شده شروع توکن شماره آبجکت است، که چیزی است که یک مدخل مرجع متقابل باید به آن اشاره کند، نه موقعیت کلمه کلیدی obj

نقشه بایتی که نشان می‌دهد جستجوی ساده سربرگ‌های شیء سه تطابق شبح‌وار درون یک رشته، یک کامنت و داده stream می‌یابد، در حالی که اسکنر توکن PDF Library for Delphi فقط سربرگ‌های واقعی را ثبت می‌کند
رشته‌ها و توضیحات و بایت‌های استریم می‌توانند هدر اشیاء را بی‌نقص تقلید کنند؛ اسکنر پس به‌جای تطبیق الگو توکنایز می‌کند و فقط توالی‌های کامل N G 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;

جزئیات مهم این است که وضعیت توکن و وضعیت رشته از یک مرز بلوک جان سالم به‌در می‌برند. یک سربرگ که خط ۶۵۵۳۶بایتی را عبور کند همچنان تشخیص داده می‌شود، چون توکن جزئی، جفت عدد صحیح در‌انتظار، و پرچم‌های درون‌رشته همه به بلوک بعدی حمل می‌شوند. بافرها ثابت‌اند: ۶۴ کیلوبایت برای اسکن، ۳۲ بایت برای طولانی‌ترین توکنی که ممکن است اصلاً اهمیت داشته باشد، و تنها آرایه‌هایی که با فایل رشد می‌کنند فهرست‌های شماره آبجکت، شماره نسل و آفست ۶۴بیتی هستند، که با تعداد واقعی آبجکت متناسب‌اند نه با اندازه فایل. در عمل، اسکن خواندن‌های پیوسته و حداکثر دو seek صریح روی کل سند صادر می‌کند، که چیزی است که آن را روی ورودی‌های چند صدمگابایتی که در مقاله ادغام و تقسیم دسترسی مستقیم بحث شده عملی می‌کند

چرا نمی‌توان اعتماد کرد یک جریان دقیقاً در endstream پایان می‌یابد؟

چون داده جریان بایت‌های دلخواه است، و بایت‌های دلخواه می‌توانند تصادفاً endstream املا کنند. یک جریان که پس از کلمه کلیدی stream شروع می‌شود باید به‌عنوان داده مات رد شود تا وقتی واقعاً پایان یابد، اما اولین وقوع کلمه کلیدی بستن فقط یک نامزد است. PDF Library for Delphi این را با الزام تأیید حل می‌کند: یک توکن endstream فقط وقتی به‌عنوان پایان واقعی جریان پذیرفته می‌شود که توکن غیر-فاصله-سفید بعدی یک endobj مستقل باشد، دنباله‌ای که §7.3.8 پیرامون یک آبجکت جریان الزام می‌کند. یک برخورد تصادفی درون داده فشرده تقریباً هرگز آن پیگیری را ندارد. اسکنر بنابراین درون جریان می‌ماند و ادامه می‌دهد. دو قاعده کوچک‌تر به‌همان‌اندازه اهمیت دارند. کلمه کلیدی stream فقط وقتی یک کلمه کلیدی خالص باشد وارد وضعیت جریان می‌شود، پس یک آبجکت نام مانند /stream در یک دیکشنری هرگز آن را فعال نمی‌کند. و یک توکن obj یا trailer فقط وقتی معتبر شمرده می‌شود که توکن از سقف ۳۲بایتی سرریز نکرده باشد و با یک اسلش شروع نشده باشد. بدون آن دو نگهبان، یک دیکشنری منبع با نام‌های کلید اشتباه کافی می‌بود تا اسکن را منحرف کند، که دقیقاً همان کلاس ورودی خصمانه است که در یادداشت‌های تجزیه PDF نامعتمد به‌طور امن پوشش داده شده

حلقه تصمیم که پایان واقعی یک stream در PDF را در حین بازیابی xref در Delphi تأیید می‌کند: توکن endstream فقط وقتی پذیرفته می‌شود که یک endobj مستقل به دنبالش بیاید
نامزد endstream به تأیید یک endobj تنهاِ پس از خودش نیاز دارد؛ محافظ‌های کلیدواژه برهنه و 32 بایتی و solidus مانع می‌شوند دیکشنری‌های دست‌ساز اسکن را از مسیر منحرف کنند

یافتن پایان واقعی دیکشنری trailer

بازیابی آبجکت‌ها فقط نیمی از کار است، چون بارگذار همچنان به یک trailer نیاز دارد تا /Root را بیابد. PDF Library for Delphi آخرین ۶۴ موقعیت کلمه کلیدی trailer یافته‌شده در طول اسکن را به‌خاطر می‌آورد و آن‌ها را به‌عقب، تازه‌ترین اول، اعتبارسنجی می‌کند، پس جدیدترین trailer قابل‌استفاده برنده می‌شود و یک کلمه کلیدی سرگردان که یک دیکشنری به‌دنبال ندارد به‌سادگی در اعتبارسنجی شکست می‌خورد و به نامزد قبلی می‌افتد. هر نامزد با یک سقف ۱ مگابایتی خوانده می‌شود، و پایان دیکشنری با پیمایش عمق تودرتوی << و >> به‌همراه گریزهای رشته تحت‌اللفظی، رشته‌های شانزده‌ده‌تایی و کامنت‌ها یافته می‌شود

// یک خواننده ساده‌لوح که در اولین '>>' متوقف می‌شود این trailer را قطع می‌کند،
// و یک پنجره ثابت 2048بایتی می‌تواند آن را در یک فایل بزرگ نصف کند
'trailer'#10 +
'<< /Size 5 /Root 1 0 R' +
'   /Custom << /Text (value >> preserved) >> >>'#10

پیمایش عمق آکادمیک نیست. یک trailer بریده‌شده که /Encrypt را از‌دست‌بدهد یک سند رمزنگاری‌شده قابل‌بازیابی را به یک سند غیرقابل‌بازشدن تبدیل می‌کند، و از‌دست‌دادن /Info یا یک زیردیکشنری سفارشی در سکوت فراداده‌ای را دور می‌ریزد که یک سیستم پایین‌دستی ممکن است به آن متکی باشد. اگر فایل رمزنگاری‌شده باشد، trailer بازیابی‌شده چیزی است که به مسیر اعتبارنامه معمولی اجازه اجرا می‌دهد، و معناهای تلاش‌مجدد همان‌هایی هستند که در مقاله بارگذاری سند رمزنگاری‌شده شرح داده شده

چه چیزی را بازسازی نمی‌تواند به شما پس دهد

بازسازی یک تلاش نهایت‌سعی است، و صادق‌بودن درباره محدودیت‌هایش بخشی از انتشارش است. سه مورد کاملاً شکست می‌خورند. آبجکت‌های بسته‌بندی‌شده درون جریان‌های آبجکت (§7.5.7) به‌طور منفرد برای یک اسکن بایتی مرئی نیستند، پس اگر یک ظرف جان‌سالم بماند اما جریان مرجع متقابلش (§7.5.8) نه، آبجکت‌هایی که نگه می‌دارد توسط بازسازی نمایه نمی‌شوند. فایلی که بدنه‌اش واقعاً فاسد شده، نه صرفاً غلط‌نمایه، سربرگ‌هایی تولید می‌کند که محتوایشان دیگر تجزیه نمی‌شود. و فایلی بدون کلمه کلیدی trailer قابل‌بازیابی و بدون کاتالوگ قابل‌خواندن هیچ چیزی برای مقیدکردن یک درخت سند به آن ندارد، صرف‌نظر از اینکه چند سربرگ آبجکت یافت شده

شماره‌های آبجکت تکراری مورد میانه جالب‌اند. یک فایل به‌طور افزایشی به‌روزرسانی‌شده قانونی چند نسل از همان شماره آبجکت را در بر دارد، و زنجیره مرجع متقابل بازمانده تنها رکورد از این است که کدام یک جاری بود. یک بازسازی آن زنجیره را ندارد، پس هر سربرگی که می‌بیند را به‌ترتیب فایل ثبت می‌کند و پس‌ازآن با شماره آبجکت حل می‌کند. معمولاً نسخه دیرتر برنده می‌شود، که معمولاً درست است، اما سندی که به‌روزرسانی شده و سپس تا حدی بازگردانده شده می‌تواند به‌شکلی متفاوت از آنچه xref اصلی توصیف کرده بازگردد. فایل‌های خطی‌شده همان هشدار را از سوی دیگر حمل می‌کنند: چیدمان صفحه‌اول و جدول‌های راهنما وقتی شاخص بازتولید شد بی‌معنا می‌شوند، پس یک فایل تعمیرشده باید به‌عنوان یک سند ساده، غیرخطی، رفتار شود

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');   // یک xref تمیز می‌نویسد
    end;
  finally
    Pdf.Free;
  end;
end;

بازگشت خودکار است: PDF Library for Delphi اسکن خام را هرجا زنجیره مرجع متقابل قابل‌خواندن نباشد اجرا می‌کند، و همچنین وقتی هر مدخل در‌حال‌استفاده آفست صفر ادعا کند، که امضای جدولی است که نوشته شده اما هرگز پر نشده. GetDocumentRepaired وقتی آن مسیر اجرا شده ۱ برمی‌گرداند، و ارزش لاگ‌کردن دارد نه نادیده‌گرفتن، چون سندی که از راه بازسازی بارگذاری شده باید دوباره در یک فایل تمیز ذخیره شود نه اینکه در یک خط‌لوله انگار هیچ اتفاقی نیفتاده رها شود. ذخیره‌کردنش یک جدول مرجع متقابل تازه و منسجم می‌نویسد، که ارزان‌ترین رفع ممکن برای هر مصرف‌کننده پایین‌دستی است

مسیر بازسازی، پرچم GetDocumentRepaired و بارگذار استریمینگ که در اینجا نشان داده شد بخشی از کتابخانه PDF Library for Delphi Delphi PDF هستند، در کنار APIهای تجزیه، رندرینگ و امضا که در جای دیگری از این بلاگ پوشش داده شده‌اند