مقاله فنی

درخت‌های Name در PDFlibPas: cycle و Limits خراب و برگ عظیم

‏PDFlibPas، کتابخانهٔ PDF losLab برای Delphi، از v3.539.45 درخت‌های name و number در PDF را با یک پشتهٔ صریح و یک مجموعهٔ ملاقات‌شده می‌پیماید، پس /Kids حلقوی و فرزندان مشترک و درخت‌هایی که هزاران سطح عمق دارند دیگر پشتهٔ فراخوانی را نمی‌کشند و مدخل تکراری تولید نمی‌کنند. از v3.539.51 جفت /Limits گمشده یا بدشکل یا معکوس هرگز شاخه‌ای که کلید را دارد پنهان نمی‌کند. مقصدهای نام‌دار و برچسب‌های صفحه و پیوست‌ها و JavaScript سطح سند همگی از این دو مسیر کد می‌خوانند، و همین آن‌ها را بخشی از attack surface هر PDF ای می‌کند که خودت تولیدش نکرده‌ای

محرک به‌ندرت عجیب است. یک fuzzer یا یک آپلود خصمانه یا یک ذخیرهٔ افزایشی باگ‌دار یک درایهٔ /Kids می‌نویسد که به جدش اشاره می‌کند، و یک پیمایگر بازگشتی روی یک فایل دو کیلوبایتی با stack overflow می‌میرد. خرابی آرام‌تر یک lookup است که به یک آرایهٔ /Limits شکسته اعتماد می‌کند و برای مقصدی که واضحاً آنجاست «یافت نشد» گزارش می‌دهد

درخت‌های name و number کجای PDF پیدایشان می‌شود؟

درخت‌های name و number هر جا که PDF مجموعه بزرگی از کلیدها را به اشیاء نگاشت می‌کند پیدایشان می‌شود، و PDFlibPas دست‌کم چهار تایشان را از طریق APIهای عمومی می‌خواند. ‏ISO 32000-1 §7.9.6 درخت name را تعریف می‌کند (کلیدهای رشته‌ای، جدول 36) و §7.9.7 درخت number را (کلیدهای صحیح، جدول 37). هر دو درخت‌هایی تقریباً متوازن‌اند که ریشه و گره‌های میانی‌شان /Kids دارند، برگ‌هایشان جفت‌های مرتب کلید/مقدار را در /Names یا /Nums حمل می‌کنند، و گره‌های غیرریشه‌شان یک آرایهٔ /Limits دوم عضوی با کوچک‌ترین و بزرگ‌ترین کلید زیرشان دارند

درختکجا زندگی می‌کندمشخصهAPI خواندن PDFlibPas
مقصدهای نام‌دار‏/Dests در dictionary نام‌ها§12.3.2.3‏GetNamedDestination، بعد GetDestPage / ‏GetDestType
برچسب‌های صفحه‏/PageLabels در catalog (درخت number)§12.4.2GetPageLabel
پیوست‌ها‏/EmbeddedFiles در dictionary نام‌ها§7.7.4، ‏§7.11.4‏EmbeddedFileCount، ‏GetEmbeddedFileStrProperty
JavaScript سطح سند‏/JavaScript در dictionary نام‌ها§7.7.4‏GlobalJavaScriptCount، ‏GlobalJavaScriptPackageName

دو جزئیات در آن جدول به‌راحتی از چشم می‌افتند. مقصدهای نام‌دار یک فرم قدیمی‌تر PDF 1.1 هم دارند، یک dictionary سادهٔ /Dests در catalog با کلیدهای شیء نام، و GetNamedDestination قبل از فرورفتن به درخت name در PDF 1.2 اول آن dictionary را چک می‌کند. و GetDocJavaScript اصلاً خوانندهٔ درخت name نیست: اسکریپت‌های چسبیده به تریگرهای سند در dictionary /AA کاتالوگ را برمی‌گرداند (WS، ‏DS، ‏WP، ‏DP، ‏DC)، در حالی که بسته‌های اسکریپت نام‌داری که وقتی سند باز می‌شود اجرا می‌شوند در درخت name یعنی /JavaScript زندگی می‌کنند

هر بایت از آن ساختارها از فایل می‌آید. مشخصه می‌گوید یک نویسنده چه چیزی باید تولید کند؛ نمی‌تواند جلوی خواننده‌ای را بگیرد که چیز دیگری دریافت می‌کند، که همان درسی است پشت سخت‌سازی یک parser PDF پاسکال در برابر فایل‌های مخرب، اینجا اعمال‌شده به شکل درخت نه اندازهٔ بافرها

چرا یک آرایهٔ /Kids حلقوی یک پیمایگر بازگشتی درخت را crash می‌کند؟

یک آرایهٔ /Kids حلقوی یک پیمایگر بازگشتی را crash می‌کند چون هیچ‌چیز در بازگشت متوجه نمی‌شود قبلاً گره‌ای را دیده، پس فرزندی که به جد خودش ارجاع می‌دهد یک فایل متناهی را به یک فرورفتن نامتناهی تبدیل می‌کند. قبل از v3.539.45، ‏NameTreeLookup و NumTreeLookup و EnumNumTree و TPDFNameTree.ProcessNode داخلی همه به‌ازای هر فرزند یک بار خودشان را صدا می‌زدند. یک ارجاع به خود کافی بود که فرایند را تمام کند، و یک درخت مشروع اما خیلی عمیق هم بدون هیچ cycle ای می‌توانست همان کار را بکند

یک واریانت ملایم‌تر به‌جای crash نتیجه را خراب می‌کند. وقتی دو درایهٔ /Kids به یک برگ ارجاع می‌دهند، یک شمارش ساده‌لوحانه دو بار به آن سر می‌زند، و یک شمارندهٔ پیوست یا فهرست بسته‌های اسکریپت مدخل‌هایی را گزارش می‌کند که وجود ندارند

فیکس بازگشت را با یک پشتهٔ صریح آخرین-داخل-اولین-خروج روی heap و یک مجموعهٔ ملاقات‌شده با کلید identity dictionary جایگزین می‌کند. گره وقتی pop می‌شود علامت می‌خورد نه وقتی push می‌شود، پس یک ارجاع حلقوی می‌تواند مدت کوتاهی روی پشته بنشیند اما لحظه‌ای که برمی‌گردد بالا دور انداخته می‌شود. هر گره متمایز فرزندانش را دقیقاً یک بار باز می‌کند، که کل کار را به تعداد dictionaryهای متمایز به‌علاوهٔ طول کل آرایه‌های /Kids شان محدود می‌کند. عمق دیگر مهم نیست: یک زنجیرهٔ 4,096 سطحی فقط 4,096 تکرار حلقه و 4,096 مدخل در یک hash set است

پیمایش درخت name در PDFlibPas که در آن آرایهٔ Kid ای که به ریشه برمی‌گشت یک پیمایگر بازگشتی را با stack overflow کشته بود، از v3.539.45 با یک پشتهٔ صریح و مجموعهٔ ملاقات‌شده جایگزین شده که گره‌ها را روی pop علامت می‌زند، فرزندان را راست‌به‌چپ push می‌کند و برگ‌ها را به‌ترتیب فایل برای GetPageLabel نگه می‌دارد
وقتی بازگشت به حلقه تبدیل می‌شود عمق دیگر مهم نیست: زنجیرهٔ 4,096 سطحی فقط 4,096 تکرار و 4,096 مدخل hash set است

اما ترتیب هنوز مهم است و پشته باید معکوس تغذیه شود تا حفظ شود. فرزندان از آخرین اندیس به اولی push می‌شوند، پس چپ‌ترین فرزند اول pop می‌شود و برگ‌ها به همان ترتیب چپ‌به‌راستی بیرون می‌آیند که تولیدکننده نوشته. ‏GetPageLabel به همین وابسته است: هر بازهٔ شمارش‌شده را می‌پیماید و آخری را اعمال می‌کند که اندیس شروعش روی یا زیر صفحه باشد، پس معکوس کردن شمارش بی‌سروصدا سبک مقدمات جلدی را به صفحهٔ 200 می‌داد. اسکلت زیر الگو را روی یک نوع گرهٔ انتزاعی نشان می‌دهد، مستقل از هر مدل شیء PDF

uses
  System.Generics.Collections;

type
  TTreeNode = class
  public
    Kids: TArray<TTreeNode>;   // روی برگ خالی
    Keys: TArray<string>;      // کلیدهای برگ، مرتب‌شده توسط یک تولیدکنندهٔ خوب
    Values: TArray<Integer>;   // موازی با Keys
    HasLimits: Boolean;
    LoKey, HiKey: string;
  end;

// /Limits یک سرنخ است: فقط جفت درست‌فرم و مرتب می‌تواند شاخه‌ای را حذف کند
function LimitsExclude(Node: TTreeNode; const Key: string): Boolean;
begin
  Result := Node.HasLimits and (Node.LoKey <= Node.HiKey) and
    ((Key < Node.LoKey) or (Key > Node.HiKey));
end;

function FindValue(Root: TTreeNode; const Key: string;
  out Value: Integer): Boolean;
var
  Pending: TList<TTreeNode>;
  Visited: TDictionary<TTreeNode, Byte>;
  Node: TTreeNode;
  I: Integer;
begin
  Result := False;
  Value := 0;
  if Root = nil then
    Exit;
  Pending := TList<TTreeNode>.Create;
  Visited := TDictionary<TTreeNode, Byte>.Create;
  try
    Pending.Add(Root);
    while Pending.Count > 0 do
    begin
      Node := Pending[Pending.Count - 1];
      Pending.Delete(Pending.Count - 1);
      if Visited.ContainsKey(Node) then
        Continue;                      // cycle یا فرزند مشترک: دیده‌ایم
      Visited.Add(Node, 0);
      if Length(Node.Kids) > 0 then
      begin
        // راست‌به‌چپ push کن تا چپ‌ترین فرزند اول pop شود
        for I := High(Node.Kids) downto 0 do
          if (Node.Kids[I] <> nil) and not LimitsExclude(Node.Kids[I], Key) then
            Pending.Add(Node.Kids[I]);
      end
      else
        for I := 0 to High(Node.Keys) do
          if (Node.Keys[I] = Key) and (I <= High(Node.Values)) then
          begin
            Value := Node.Values[I];
            Exit(True);
          end;
      // یافتن نشدن در این برگ حکم نیست: به pop کردن خواهرها ادامه بده
    end;
  finally
    Visited.Free;
    Pending.Free;
  end;
end;

چرا lookup نمی‌تواند روی اولین شاخهٔ منطبق توقف کند؟

یک lookup نمی‌تواند روی اولین شاخه‌ای که بازه‌اش منطبق است توقف کند، چون بازه‌های /Limits در یک فایل واقعی می‌توانند هم‌پوشانی داشته باشند یا دروغ بگویند، و شاخه‌ای که مدعی کلید است لزوماً شاخه‌ای نیست که آن را دارد. lookupهای قبل از v3.539.45 روی اولین فرزندی که /Limits اش کلید را می‌پوشاند یک فلگ Found می‌گذاشتند، داخلش فرورفته و دیگر به خواهر دیگری نگاه نمی‌کردند. اگر آن فرزند خالی درمی‌آمد یا کهنه بود یا حلقه‌ای به ریشه، جواب nil بود، حتی وقتی خواهر بعدی همان کلید را داشت

‏FindTreeValue بازنویسی‌شده که حالا پشت هر دو NameTreeLookup و NumTreeLookup است، هر فرزندی که بازه‌اش کلید را حذف نکند push می‌کند و تا پیدا کردن یک تطبیق یا خالی شدن پشته به pop کردن ادامه می‌دهد. یافتن نشدن داخل یک برگ فقط یافتن نشدن داخل یک برگ است. در درختی درست‌فرم این هیچ هزینهٔ اضافه‌ای ندارد؛ در درختی خراب چند ملاقات گره بیشتر هزینه دارد و جواب درست را برمی‌گرداند

جست‌وجوی برگ هم همین فلسفه را دنبال می‌کند. ‏ISO 32000-1 می‌خواهد کلیدهای یک آرایهٔ /Names به‌ترتیب مقدار بایت مرتب باشند، پس برگ اول با جست‌وجوی دودویی جست‌وجو می‌شود. اگر آن شکست بخورد، PDFlibPas به اسکن خطی جفت‌ها برمی‌گردد، چون برگ خارج‌ازترتیب وگرنه یک کلید حاضر را نامرئی می‌کرد. مرتب‌سازی مسیر سریع است، نه فیلتر

lookup همچنین از حدس زدن روی یک تناقض ساختاری خودداری می‌کند. جدول 36 اجازه می‌دهد گره یا /Kids داشته باشد یا /Names، هرگز هر دو را، و مسیر lookup گره‌ای که هر دو را حمل می‌کند بدشکل تلقی و آن را skip می‌کند به‌جای اینکه یک تفسیر را انتخاب کند. مسیرهای شمارش مثل EnumNumTree بخشنده‌ترند و وقتی هر دو حاضرند /Kids را دنبال می‌کنند

یک خواننده اجازه دارد برای چه به /Limits اعتماد کند؟

یک خواننده فقط برای رد کردن کار اجازه دارد به /Limits اعتماد کند، هرگز برای تصمیم گرفتن اینکه کلیدی غایب است، و فقط وقتی جفت درست‌فرم باشد. جدول 36 می‌گوید گره‌های میانی و برگ باید /Limits را به‌شکل آرایه‌ای دوم عضوی از کم‌ترین و بزرگ‌ترین کلیدها حمل کنند، اما در عمل آن درایه بعد از ویرایش‌های دستی گم می‌شود، یا اعداد را در یک درخت name نگه می‌دارد، یا با مرزهای عوض‌شده می‌رسد. ‏PDFlibPas v3.539.45 و v3.539.51 هر حالت را به یک شکل حل می‌کنند: اگر بازه نتوان به‌شکل جفت مرتبی از نوع درست خوانده شود، فرزند قابل‌جست‌وجو می‌ماند

  • /Limits گمشده: چک بازهٔ قدیمی False برمی‌گرداند و فرزند کلاً skip می‌شد، پس تولیدکننده‌ای که درایه را فراموش می‌کرد کل زیردرختش را غیرقابل‌دسترس می‌کرد. از v3.539.45 فرزند جست‌وجو می‌شود
  • نوع اشتباه یا طول اشتباه، مثل اعداد در یک درخت name یا آرایه‌ای تک‌عضوی: از v3.539.45 دقیقاً مثل یک درایهٔ گمشده رفتار می‌شود
  • مرزهای معکوس مثل [(Z) (A)] یا [9 0]: ‏v3.539.45 هنوز از آن‌ها استفاده می‌کرد و هیچ کلیدی نمی‌توانست Lo <= Key <= Hi را وقتی Lo > Hi برآورده کند، پس شاخه برای هر lookup حذف می‌شد. از v3.539.51 یک بازه فقط وقتی برای حذف استفاده می‌شود که مرز پایینش از مرز بالایش تجاوز نکند
  • درست‌فرم، مرتب و درست: برای حذف شاخه استفاده می‌شود، که تمام هدف درایه همین است
قواعد PDFlibPas برای اعتماد به آرایهٔ Limits درخت name: جفت گمشده یا بدنوع یا معکوس از v3.539.45 و v3.539.51 فرزند را قابل‌جست‌وجو نگه می‌دارد، و فقط جفت مرتب درست‌فرم می‌تواند شاخه را حذف کند، پس یک Limits خصمانه می‌تواند ملاقات‌های بیشتر بخرد اما دیگر نمی‌تواند مقصدی موجود را پنهان کند
بازه‌ها می‌توانند کار را رد کنند اما هرگز غیبت را تصمیم نمی‌گیرند، چون کلیدهای واقعی ذخیره‌شده در برگ‌ها نتیجهٔ هر lookup را تعیین می‌کنند

کلیدهای واقعی در هر حالتی نتیجه را تعیین می‌کنند. یک /Limits خصمانه می‌تواند PDFlibPas را وادار کند گره‌های بیشتری از لازم ببیند، اما یک ‏/Limits بدشکل دیگر نمی‌تواند مقصدی موجود را ناپدید کند. از سمت فراخواننده هیچ چیزی عوض نمی‌شود: ‏GetNamedDestination وقتی نام واقعاً غایب است 0 برمی‌گرداند و در غیر این صورت یک ID مقصد، و توابع مقصد از آنجا ادامه می‌دهند

uses
  PDFlibrary;

procedure LookUpDestination(const FileName, DestName: string);
var
  Lib: TPDFlib;
  DestID: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile(FileName, '') <> 1 then
    begin
      WriteLn('Load failed, error ', Lib.LastErrorCode);
      Exit;
    end;
    // اول /Dests در کاتالوگ (PDF 1.1)، بعد درخت name یعنی /Dests
    DestID := Lib.GetNamedDestination(DestName);
    if DestID = 0 then
      WriteLn('No destination named ', DestName)
    else if Lib.GetDestPage(DestID) = 0 then
      WriteLn(DestName, ' exists but does not resolve to a page')
    else
      WriteLn(DestName, ' -> page ', Lib.GetDestPage(DestID),
        ', view type ', Lib.GetDestType(DestID));  // 1 = XYZ، 2 = Fit و ...
  finally
    Lib.Free;
  end;
end;

روی یک فایل دست‌ساز که ریشهٔ /Dests اش یک فرزند دارد که زیر بازهٔ [(a) (z)] به ریشه برمی‌گردد و یک فرزند دوم که مدخل واقعی را زیر limits معکوس یعنی [(z) (a)] نگه می‌دارد، این رویه مقصد را به صفحهٔ 2 با نوع دید 2 یعنی Fit حل می‌کند. قبل از v3.539.45 همان lookup مقدار 0 برمی‌گرداند، چون فرزند حلقه‌ای اول مدعی کلید می‌شد و جست‌وجو هرگز به خواهرش نمی‌رسید؛ v3.539.45 تنها هم همچنان 0 برمی‌گرداند، چون بازهٔ معکوس برگ واقعی را حذف می‌کرد. اگر بعدش outline ای را بخوانی که به این مقصدها اشاره می‌کند، مقالهٔ همراه دربارهٔ خواندن اکشن‌های بوکمارک و حاشیه‌نویسی PDF در Delphi سمت action را پوشش می‌دهد

برگی با 32,769 نام چطور TPDFNameTree را شکست؟

برگی با 32,769 جفت نام/مقدار ‏TPDFNameTree را می‌شکست چون FindIndex داخلی‌اش دو عدد را در یک Integer 32 بیتی بسته‌بندی می‌کرد: موقعیت برگ در فهرست آرایهٔ داخلی در 16 بیت بالا و آفست مدخل داخل آرایهٔ /Names آن برگ در 16 بیت پایین. هر جفت دو خانهٔ آرایه اشغال می‌کند، پس جفت 32,769 یعنی جفت با اندیس 32,768 از آفست 65,536 شروع می‌شود که همان $10000 است. آن مقدار به نیمهٔ بالا انتقال می‌برد و decoder آن را به‌عنوان آفست 0 در برگ بعدی می‌خواند

بسته‌بندی FindIndex در TPDFNameTree در PDFlibPas که در آن موقعیت برگ و آفست مدخل یک Integer 32 بیتی مشترک داشتند و جفت 32768 از آفست 65536 شروع می‌شد، پس انتقال به نیمهٔ بالا به‌شکل آفست 0 برگ بعدی خوانده می‌شد و FindKey یا DeleteKey جفت اشتباه را دست می‌زد در حالی که HasKey مخالف می‌گفت
دو مقدار 16 بیتی در یک عدد صحیح 32 بیتی لحظه‌ای که برگ از 32,768 جفت می‌گذرد بی‌صدا بریده می‌شوند، اندازه‌ای که مرجع‌های دستی واقعی به آن می‌رسند

‏TPDFNameTree کلاسی است که پشت پیوست‌ها و بسته‌های JavaScript سراسری و نوشتن مقصدهای نام‌دار است، که پیامدهایش را ملموس می‌کند. در درخت تک‌برگی برگ بعدی وجود ندارد، پس FindKey و DeleteKey از انتهای فهرست برگ بیرون اندیس می‌زدند؛ در درخت چندبرگی به‌جای جفت درخواستی اولین جفت برگ بعدی را برمی‌گرداندند یا حذف می‌کردند. در همین حین HasKey اسکن خودش را اجرا می‌کرد و کلید را حاضر گزارش می‌کرد، پس کلاس با خودش تناقض داشت. یک مرجع دستی تولیدشده با یک مقصد نام‌دار برای هر نماد API بدون زور از 32,768 مدخل می‌گذرد، و بعضی تولیدکننده‌ها همه را در یک برگ مسطح تکی می‌نویسند

از v3.539.45 ‏FindIndex اندیس آرایه را از طریق یک پارامتر out جدا برمی‌گرداند و آفست کامل مدخل را به‌عنوان نتیجه، پس هیچ‌کدام بریده نمی‌شوند. همان release دو همسایه را هم سخت‌گیرانه‌تر کرد. ‏KeyName حالا فقط کلیدهای رشته‌ای واقعی را می‌شمارد و برمی‌گرداند و برای اندیس 0 یا کمتر رشتهٔ خالی برمی‌گرداند، جایی که قبلاً هر شیئی را که بعد از یک کلید نامعتبر می‌آمد cast می‌کرد. ‏HasKey دیگر یک کلید عددی یا نامعتبر را نام خالی حساب نمی‌کند. برای برگ‌ای مثل [(Valid) 42 123 456]، ‏HasKey('') حالا False است و KeyName(2) رشتهٔ خالی برمی‌گرداند

procedure AuditTrees(const FileName: string);
var
  Lib: TPDFlib;
  I: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile(FileName, '') <> 1 then
      Exit;
    // درخت number یعنی /PageLabels؛ فایل‌های بدون آن شمارهٔ صفحهٔ ساده برمی‌گردانند
    for I := 1 to Lib.PageCount do
      WriteLn('Page ', I, ' label: ', Lib.GetPageLabel(I));
    // درخت name یعنی /EmbeddedFiles؛ اندیس‌ها 1-مبنا هستند، کلیدهای غیر رشته‌ای skip می‌شوند
    for I := 1 to Lib.EmbeddedFileCount do
      WriteLn('Attachment ', I, ': ', Lib.GetEmbeddedFileStrProperty(I, 1),
        ' (', Lib.GetEmbeddedFileStrProperty(I, 2), ')');  // نام، نوع MIME
    // درخت name یعنی /JavaScript: نام بسته‌ها را فهرست کن، هیچ‌چیز را اجرا نکن
    for I := 1 to Lib.GlobalJavaScriptCount do
      WriteLn('Script package: ', Lib.GlobalJavaScriptPackageName(I));
  finally
    Lib.Free;
  end;
end;

روی همان فایل دست‌ساز، که ریشهٔ /PageLabels اش یک برگ را دو بار فهرست می‌کند و به خودش ارجاع می‌دهد، این ممیزی برای دو صفحه ‏i و A-1 چاپ می‌کند، هر بازه یک بار، و بستهٔ اسکریپت تکی از یک درخت /JavaScript که آن هم به ریشهٔ خودش اشاره می‌کند. سمت نوشتن برچسب‌های صفحه تاریخچهٔ خودش را با ریشه‌های /Kids دارد، پوشش داده‌شده در فیکس کردن برچسب‌های صفحهٔ PDF ذخیره‌شده در درخت‌های number با /Kids؛ ‏AddPageLabels چنین ریشه‌ای را قبل از درج مسطح می‌کند و به همان شمارش EnumNumTree که اینجا توضیح داده شد تکیه دارد

این سخت‌سازی هنوز چه چیزی را تضمین نمی‌کند؟

سخت‌سازی پایان‌پذیری و ترتیب پایدار و نتایج درست برای درخت‌هایی که کلیدهای واقعی‌شان سالم‌اند را تضمین می‌کند؛ درخت خراب را به معنای قصد نویسنده‌اش درنمی‌آورد. چند محدودیت ارزش دانستن دارند قبل از اینکه روی آن بسازی

  • مجموعهٔ ملاقات‌شده با identity شیء کار می‌کند. دو dictionary متمایز با محتوای یکسان دو گره‌اند، پس تولیدکننده‌ای که به‌جای ارجاع دادن یک برگ را کپی می‌کند همچنان مدخل تکراری تولید می‌کند
  • یک /Limits درست‌فرم و مرتب اما غلط هنوز حذف می‌کند. خواننده‌ای که از بازه‌ها به‌عنوان بهینه‌سازی استفاده می‌کند نمی‌تواند هم‌زمان نسبت به بازه‌ای که باورپذیر دروغ می‌گوید مصون باشد؛ تنها جایگزین، نادیده گرفتن کامل /Limits و اسکن کردن هر برگ است
  • شمارش ترتیب فایل را حفظ می‌کند اما مرتب نمی‌کند. ‏GetPageLabel آخرین بازهٔ شمارش‌شدهٔ روی یا زیر صفحه را اعمال می‌کند، پس تولیدکننده‌ای که بازه‌ها را خارج از ترتیب می‌نویسد معناشناسی ترتیب‌فایل می‌گیرد
  • حافظه با تعداد گره‌ها و مدخل‌های متمایز رشد می‌کند. پیمایش یک فهرست و یک hash set اضافه می‌کند، هیچ چیز بیشتر، اما یک درخت name صد مگابایتی بعد از parse هم همچنان درخت name صد مگابایتی است
  • کلیدهای تکراری داخل یک برگ گزارش نمی‌شوند. جست‌وجوی دودویی هر جفت منطبقی که اول به آن بخورد را برمی‌گرداند؛ fallback خطی آخرین منطبقی که اسکن می‌کند را نگه می‌دارد

مرجع سریع: خواندن درخت‌های PDF از فایل‌های غیرقابل‌اعتماد

  • برای پیمایش مقاوم به cycle و مقاوم به پشتهٔ درخت‌های name و number به v3.539.45 یا بعدتر ارتقا بده، و به v3.539.51 یا بعدتر تا /Limits معکوس دیگر کلیدها را پنهان نکنند
  • 0 برگرداندن GetNamedDestination را «غایب» بگیر و 0 برگرداندن GetDestPage را «حاضر اما غیرقابل‌استفاده»
  • برای درخت name یعنی /JavaScript از GlobalJavaScriptCount و GlobalJavaScriptPackageName استفاده کن؛ ‏GetDocJavaScript به‌جایش تریگرهای /AA کاتالوگ را می‌خواند
  • پیوست‌ها و بسته‌های اسکریپت را از 1 تا شمارنده‌ای که کتابخانه گزارش می‌کند اندیس بزن؛ کلیدهای نامعتبر شمرده نمی‌شوند
  • در کد درخت خودت، گره‌ها را روی pop ملاقات‌شده علامت بزن، فرزندان را معکوس push کن، و بگذار /Limits فقط وقتی یک جفت درست‌نوع و مرتب است حذف کند

ابزارهای پیش‌پرواز و آرشیوگرها و viewerها این درخت‌ها را قبل از رندر شدن هر صفحه می‌خوانند، پس باید از هر چیزی که در صف آپلود می‌رسد جان سالم به در ببرند. خواننده‌های درخت توصیف‌شده در بالا با PDFlibPas، کتابخانهٔ PDF برای Delphi عرضه می‌شوند که با هر دو Delphi و Free Pascal بیلد می‌شود