مقاله فنی

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

وقتی یک جدول مرجع متقابل PDF غیرقابل‌استفاده است، رفع مشکل این است که کاملاً نادیده‌اش بگیرید و آن را از بدنه فایل بازسازی کنید. کتابخانه PDFlibPas Delphi PDF این کار را با یک اسکنر توکن تک‌گذر انجام می‌دهد که هر سربرگ آبجکت غیرمستقیم واقعی که می‌بیند را ثبت می‌کند، سپس دیکشنری 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 بنابراین اصلاً الگو-تطبیقی نمی‌کند. توکن‌سازی می‌کند، که یعنی همیشه می‌داند آیا بایت‌های زیر نشانگر کد هستند یا بار محتوایی، و بار محتوایی بدون اینکه هرگز تفسیر شود رد می‌شود

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

PDFlibPas کل فایل را دقیقاً یک‌بار، در بلوک‌های ۶۴ کیلوبایتی، با یک ماشین حالت ساخته‌شده روی قواعد توکن 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;

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

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

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

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

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

// 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 قابل‌بازیابی و بدون کاتالوگ قابل‌خواندن هیچ چیزی برای مقیدکردن یک درخت سند به آن ندارد، صرف‌نظر از اینکه چند سربرگ آبجکت یافت شده

شماره‌های آبجکت تکراری مورد میانه جالب‌اند. یک فایل به‌طور افزایشی به‌روزرسانی‌شده قانونی چند نسل از همان شماره آبجکت را در بر دارد، و زنجیره مرجع متقابل بازمانده تنها رکورد از این است که کدام یک جاری بود. یک بازسازی آن زنجیره را ندارد، پس هر سربرگی که می‌بیند را به‌ترتیب فایل ثبت می‌کند و پس‌ازآن با شماره آبجکت حل می‌کند. معمولاً نسخه دیرتر برنده می‌شود، که معمولاً درست است، اما سندی که به‌روزرسانی شده و سپس تا حدی بازگردانده شده می‌تواند به‌شکلی متفاوت از آنچه 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');   // writes a clean xref
    end;
  finally
    Pdf.Free;
  end;
end;

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

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