บทความเทคนิค

PDFlibPas เดิน name tree กับ number tree แบบทน cycle

PDFlibPas ซึ่งเป็น losLab PDF Library for Delphi เดิน PDF name tree กับ number tree ด้วย explicit stack กับ visited set ตั้งแต่ v3.539.45 /Kids ที่วนเป็นวง ลูกที่ถูกแชร์ และ tree ที่ลึกเป็นพันชั้นจึงไม่ทำให้ call stack หมด หรือทำให้ entry ซ้ำอีกต่อไป ตั้งแต่ v3.539.51 คู่ /Limits ที่หายไป ผิดรูป หรือกลับด้าน ก็ไม่มีทางซ่อนกิ่งที่ถือ key อีกเช่นกัน named destination, page label, attachment และ JavaScript ระดับเอกสารล้วนอ่านผ่านเส้นทางโค้ดสองทางนี้ ซึ่งทำให้พวกมันเป็นส่วนหนึ่งของ attack surface ของ PDF ทุกฉบับที่คุณไม่ได้ผลิตเอง

ตัวกระตุ้นมักไม่ใช่ของแปลก ๆ fuzzer, ไฟล์อัปโหลดที่มีเจตนาร้าย หรือ incremental save ที่บั๊ก เขียน entry /Kids ที่ชี้กลับไปหาบรรพบุรุษ และตัวเดินแบบ recursive ก็ตายด้วย stack overflow บนไฟล์สองกิโลไบต์ ส่วนความพังที่เงียบกว่าคือ lookup ที่เชื่อ array /Limits ที่พัง แล้วรายงานว่า "ไม่พบ" สำหรับ destination ที่อยู่ตรงนั้นจะ ๆ

name tree กับ number tree อยู่ตรงไหนใน PDF

name tree กับ number tree อยู่ทุกที่ที่ PDF จับชุด key ใหญ่ ๆ ไปแมปกับ object และ PDFlibPas อ่านอย่างน้อยสี่ตัวผ่าน public API ISO 32000-1 §7.9.6 นิยาม name tree (key เป็น string, Table 36) และ §7.9.7 นิยาม number tree (key เป็นจำนวนเต็ม, Table 37) ทั้งคู่เป็น tree แบบค่อนข้างสมดุลที่ node รากกับ node ระดับกลางถือ /Kids leaf ถือคู่ key/value ที่เรียงแล้วใน /Names หรือ /Nums และ node ที่ไม่ใช่รากถือ array /Limits สองช่องบอก key เล็กสุดกับใหญ่สุดที่อยู่ใต้มัน

Treeอยู่ที่ไหนสเปกread API ของ PDFlibPas
named destination/Dests ใน name dictionary§12.3.2.3GetNamedDestination ตามด้วย GetDestPage / GetDestType
page label/PageLabels ใน catalog (number tree)§12.4.2GetPageLabel
attachment/EmbeddedFiles ใน name dictionary§7.7.4, §7.11.4EmbeddedFileCount, GetEmbeddedFileStrProperty
JavaScript ระดับเอกสาร/JavaScript ใน name dictionary§7.7.4GlobalJavaScriptCount, GlobalJavaScriptPackageName

รายละเอียดสองจุดในตารางนี้พลาดง่าย named destination ยังมีรูปเก่าของ PDF 1.1 คือ dictionary /Dests ธรรมดาใน catalog ที่ใช้ name object เป็น key และ GetNamedDestination จะเช็ค dictionary นั้นก่อนแล้วค่อยลงไปเดิน name tree ของ PDF 1.2 ส่วน GetDocJavaScript ไม่ใช่ตัวอ่าน name tree เลย มันคืน script ที่แปะกับ document trigger ใน dictionary /AA ของ catalog (WS, DS, WP, DP, DC) ขณะที่ package ของ script ที่มีชื่อซึ่งรันเมื่อเอกสารเปิด อยู่ใน name tree ของ /JavaScript

ไบต์ทุกตัวของโครงสร้างพวกนี้มาจากไฟล์ สเปกบอกว่า writer ต้องผลิตอะไร แต่มันห้าม reader ไม่ได้ว่าจะได้รับอย่างอื่น ซึ่งเป็นบทเรียนเดียวกับเบื้องหลังการทำให้ Pascal PDF parser แข็งแรงขึ้นต้านไฟล์ประสงค์ร้าย แค่ยกมาใช้กับรูปทรงของ tree แทนขนาด buffer

ทำไม array /Kids ที่วนเป็นวงถึงทำ recursive tree walker ล้ม

array /Kids ที่วนเป็นวงทำ recursive walker ล้มเพราะไม่มีอะไรใน recursion สังเกตว่าตัวเองเคยเห็น node ตัวนี้แล้ว ลูกที่อ้างถึงบรรพบุรุษของตัวเองจึงเปลี่ยนไฟล์จำกัด ๆ ให้กลายเป็นการดิ่งไม่สิ้นสุด ก่อน v3.539.45 NameTreeLookup, NumTreeLookup, EnumNumTree และ TPDFNameTree.ProcessNode ภายใน ต่างเรียกตัวเองหนึ่งครั้งต่อลูก การอ้างตัวเองสักครั้งเดียวก็พอจะจบ process และ tree ที่ถูกต้องแต่ลึกมาก ๆ ก็ทำแบบเดียวกันได้โดยไม่ต้องมีวงเลย

เวอร์ชันอ่อนโยนกว่านั้นไม่ทำให้ล้ม แต่ทำให้ผลลัพธ์เสีย เมื่อ entry /Kids สองตัวอ้าง leaf ตัวเดียวกัน การนับแบบโง่ ๆ จะเยือนมันสองครั้ง และจำนวน attachment หรือรายชื่อ script package ก็จะรายงาน entry ที่ไม่มีอยู่จริง

การแก้เปลี่ยน recursion เป็น stack แบบ last-in, first-out ชัดเจนบน heap พร้อม visited set ที่ใช้ identity ของ dictionary เป็น key node จะถูก mark เมื่อมันถูก pop ไม่ใช่ตอนถูก push การอ้างวนเป็นวงจึงนั่งอยู่บน stack ได้แค่ครู่หนึ่ง แล้วถูกทิ้งทันทีที่มันลอยขึ้นมาใหม่ node ตัวเฉพาะแต่ละตัวขยายลูกของมันเพียงครั้งเดียว งานรวมจึงถูกผูกด้วยจำนวน dictionary ที่ต่างกันบวกความยาวรวมของ array /Kids ทั้งหมด ความลึกเลิกสำคัญ: ห่วงโซ่ 4,096 ชั้นก็แค่ 4,096 รอบของ loop กับ 4,096 entry ใน hash set

การเดิน name tree ของ PDFlibPas ที่ array Kid วนกลับไปหารากเคยฆ่า recursive walker ด้วย stack overflow ตั้งแต่ v3.539.45 ถูกแทนด้วย explicit stack กับ visited set ที่ mark node ตอน pop push ลูกจากขวาไปซ้าย และคง leaf ไว้ตามลำดับไฟล์เพื่อ GetPageLabel
ความลึกเลิกมีความหมายเมื่อ recursion กลายเป็น loop ห่วงโซ่ 4,096 ชั้นก็แค่ 4,096 รอบกับ 4,096 entry ใน hash set

แต่ลำดับก็ยังสำคัญ และ stack ต้องถูกป้อนย้อนทางเพื่อคงมันไว้ ลูกถูก push จาก index สุดท้ายลงมาหาตัวแรก ลูกซ้ายสุดจึงถูก pop ก่อน และ leaf ออกมาตามลำดับซ้ายไปขวาเดียวกับที่ producer เขียนไว้ GetPageLabel พึ่งพาเรื่องนี้: มันเดิน range ที่นับได้ทุกช่วงแล้วใช้ช่วงสุดท้ายที่จุดเริ่มอยู่ที่หรือต่ำกว่าหน้านั้น ถ้าลำดับการนับกลับด้าน หน้า 200 จะได้ style ของส่วนหน้าเฉย ๆ ไปโดยไม่บอก skeleton ด้านล่างโชว์รูปแบบนี้บนชนิด node นามธรรม ไม่ผูกกับ object model ของ PDF ตัวไหน

uses
  System.Generics.Collections;

type
  TTreeNode = class
  public
    Kids: TArray<TTreeNode>;   // ว่างบน leaf
    Keys: TArray<string>;      // key ของ leaf เรียงโดย producer ที่ประพฤติดี
    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;
      // ไม่เจอใน leaf นี้ยังไม่ใช่คำตัดสิน: ค่อย ๆ pop พี่น้องต่อไป
    end;
  finally
    Visited.Free;
    Pending.Free;
  end;
end;

ทำไม lookup หยุดที่กิ่งแรกที่ range ตรงไม่ได้

lookup หยุดที่กิ่งแรกที่ range ตรงไม่ได้ เพราะ range ของ /Limits ในไฟล์จริงซ้อนทับกันหรือโกหกได้ กิ่งที่อ้างว่าถือ key ไม่จำเป็นต้องเป็นกิ่งที่ถือมันจริง lookup ก่อน v3.539.45 เซ็ต flag Found ที่ลูกแรกที่ /Limits ครอบคลุม key ลงไปเดินต่อในกิ่งนั้น แล้วไม่เคยมองพี่น้องตัวอื่นอีก ถ้าลูกตัวนั้นกลายเป็นก้อนว่าง ค้างสเตล หรือวนกลับไปหาราก คำตอบก็คือ nil แม้พี่น้องตัวถัดไปจะถือ key อยู่ก็ตาม

FindTreeValue ที่เขียนใหม่ซึ่งตอนนี้รองรับทั้ง NameTreeLookup กับ NumTreeLookup push ลูกทุกตัวที่ range ไม่ตัด key ออก แล้วค่อย ๆ pop จนกว่าจะเจอ match หรือ stack ว่าง การไม่เจอใน leaf เดียวก็แค่ไม่เจอใน leaf เดียว บน tree ที่สมบูรณ์ค่าใช้จ่ายเพิ่มเป็นศูนย์ บน tree ที่พังก็แค่เยือน node เพิ่มอีกไม่กี่ตัวแล้วได้คำตอบที่ถูก

การค้นใน leaf ยึดปรัชญาเดียวกัน ISO 32000-1 บังคับว่า key ใน array /Names ต้องเรียงตามค่าไบต์ leaf จึงถูกค้นด้วย binary search ก่อน ถ้าไม่เจอ PDFlibPas จะ fallback ไปสแกนเชิงเส้นทีละคู่ เพราะ leaf ที่เรียงพลาดจะทำให้ key ที่อยู่จริงมองไม่เห็นไปเฉย ๆ การเรียงคือ fast path ไม่ใช่ตัวกรอง

lookup ยังปฏิเสธที่จะเดาในข้อขัดแย้งเชิงโครงสร้างหนึ่งเรื่อง Table 36 อนุญาตให้ node ถือ /Kids หรือ /Names อย่างใดอย่างหนึ่งเท่านั้น ห้ามทั้งคู่ เส้นทาง lookup จึงถือ node ที่ถือทั้งสองเป็นไฟล์ผิดรูปแล้วข้ามมันไป แทนที่จะเลือกตีความสักอย่าง เส้นทางนับอย่าง EnumNumTree หย่อนกว่าและไล่ตาม /Kids เมื่อทั้งคู่มีอยู่

reader เชื่อ /Limits เพื่ออะไรได้บ้าง

reader เชื่อ /Limits ได้แค่เพื่อตัดงานทิ้ง ห้ามใช้ตัดสินว่า key นั้นไม่มี และใช้ได้เฉพาะเมื่อคู่นั้นสมบูรณ์ Table 36 บอกว่า node ระดับกลางกับ leaf ต้องถือ /Limits เป็น array สองช่องของ key เล็กสุดกับใหญ่สุด แต่ในทางปฏิบัติ entry นี้หายหลังการแก้ไฟล์มือ ใส่ตัวเลขไว้ใน name tree หรือมาถึงพร้อมขอบเขตกลับด้าน PDFlibPas v3.539.45 กับ v3.539.51 จัดการทุกเคสแบบเดียวกัน: ถ้า range อ่านเป็นคู่เรียงลำดับของชนิดที่ถูกไม่ได้ ลูกก็ยังถูกค้นต่อ

  • /Limits หาย: เช็ค range แบบเดิมคืน False และลูกถูกข้ามทิ้งเลย producer ที่ลืม entry เลยทำ subtree ทั้งก้อนไปไม่ถึง ตั้งแต่ v3.539.45 ลูกถูกค้น
  • ชนิดผิดหรือความยาวผิด อย่างตัวเลขใน name tree หรือ array ช่องเดียว: ถือเหมือน entry หายตั้งแต่ v3.539.45
  • ขอบเขตกลับด้านอย่าง [(Z) (A)] หรือ [9 0]: v3.539.45 ยังใช้พวกมันอยู่ และไม่มี key ใดพอใจ Lo <= Key <= Hi เมื่อ Lo > Hi กิ่งจึงถูกตัดทิ้งในทุก lookup ตั้งแต่ v3.539.51 range จะถูกใช้ตัดงานเมื่อขอบล่างไม่เกินขอบบนเท่านั้น
  • สมบูรณ์ เรียงถูกทิศ และถูกต้อง: ใช้ตัดกิ่งได้ ซึ่งก็คือจุดประสงค์ทั้งหมดของ entry นี้
กฎของ PDFlibPas ในการเชื่อ array Limits ของ name tree: คู่ที่หาย ชนิดผิด หรือกลับด้าน ปล่อยให้ลูกถูกค้นต่อตั้งแต่ v3.539.45 กับ v3.539.51 และมีแต่คู่ที่สมบูรณ์และเรียงถูกทิศเท่านั้นที่ตัดกิ่งได้ Limits ที่มุ่งร้ายจึงทำให้เสียเวลาเยือนเพิ่มได้ แต่ซ่อน destination ที่มีอยู่จริงไม่ได้อีก
range ตัดงานได้ แต่ห้ามตัดสินความไม่มีอยู่เด็ดขาด เพราะ key จริงที่เก็บอยู่ใน leaf ต่างหากที่ตัดสินผลของทุก lookup

key จริงตัดสินผลในทุกเคส /Limits ที่มุ่งร้ายทำให้ PDFlibPas เยือน node เกินจำเป็นได้ แต่ตัวที่ผิดรูปไม่มีทางทำให้ destination ที่มีอยู่จริงหายไปได้อีก จากฝั่ง caller ไม่มีอะไรเปลี่ยน: GetNamedDestination คืน 0 เมื่อชื่อนั้นไม่มีจริง และคืน destination ID เมื่อมี ส่วนฟังก์ชันของ destination ดำเนินต่อจากตรงนั้น

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;
    // Catalog /Dests (PDF 1.1) ก่อน แล้วค่อย name tree ของ /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 มีลูกหนึ่งตัววนกลับไปหารากภายใต้ range [(a) (z)] และลูกตัวที่สองถือ entry จริงอยู่ใต้ limits แบบกลับด้าน [(z) (a)] procedure นี้ resolve destination ได้หน้า 2 พร้อม view type 2 (Fit) ก่อน v3.539.45 lookup เดียวกันคืน 0 เพราะลูกที่วนเป็นวงอ้างสิทธิ์ key ก่อน และการค้นไม่เคยไปถึงพี่น้องของมัน v3.539.45 อย่างเดียวยังคืน 0 เพราะ range ที่กลับด้านตัด leaf จริงทิ้ง ถ้าคุณจะอ่าน outline ที่ชี้ไปหา destination พวกนี้ต่อ บทความคู่กันเรื่องการอ่าน action ของ bookmark และ annotation ใน PDF ด้วย Delphi เล่าฝั่ง action ให้ครบ

leaf ที่มี 32,769 ชื่อทำ TPDFNameTree พังอย่างไร

leaf ที่มีคู่ name/value จำนวน 32,769 ทำ TPDFNameTree พังเพราะ FindIndex ภายในของมันอัดตัวเลขสองตัวลง Integer 32 บิตตัวเดียว: ตำแหน่งของ leaf ใน array list ภายในอยู่ครึ่งสูง 16 บิต และ offset ของ entry ใน array /Names ของ leaf นั้นอยู่ครึ่งต่ำ 16 บิต แต่ละคู่กินสองช่องของ array คู่ที่ 32,769 คือ pair index 32,768 จึงเริ่มที่ offset 65,536 หรือ $10000 ค่านี้พาดพิงเข้าครึ่งสูง และ decoder อ่านมันกลับมาเป็น offset 0 ของ leaf ถัดไป

การอัดค่าของ TPDFNameTree FindIndex ใน PDFlibPas ที่ตำแหน่ง leaf กับ offset ของ entry ต้องแชร์ Integer 32 บิตตัวเดียว คู่ที่ 32768 เริ่มที่ offset 65536 พาดพิงเข้าครึ่งสูงแล้วถูกอ่านกลับเป็น offset 0 ของ leaf ถัดไป FindKey หรือ DeleteKey เลยแตะคู่ผิดตัวขณะ HasKey บอกสวนทาง
ค่า 16 บิตสองตัวใน integer 32 บิตตัดทอนแบบเงียบ ๆ ทันทีที่ leaf ข้าม 32,768 คู่ ซึ่งเป็นขนาดที่ reference manual จริง ๆ ไปถึง

TPDFNameTree เป็นคลาสเบื้องหลัง attachment, global JavaScript package และการเขียน named destination ผลที่ตามมาจึงเป็นรูปธรรม บน tree ใบเดียวไม่มี leaf ถัดไปให้ไป FindKey กับ DeleteKey จึง index ล้ำเลยสุดของรายการ leaf บน tree หลายใบพวกมันคืนหรือลบคู่แรกของ leaf ถัดไปแทนที่จะเป็นคู่ที่ขอไป ระหว่างนั้น HasKey วิ่งสแกนของตัวเองแล้วรายงานว่า key อยู่ คลาสจึงพูดสวนตัวเอง reference manual ที่สร้างอัตโนมัติที่มี named destination หนึ่งตัวต่อ symbol ของ API ข้าม 32,768 entry ได้โดยไม่ต้องตั้งใจ และบาง producer เขียนมันทั้งหมดลง leaf แบนใบเดียว

ตั้งแต่ v3.539.45 FindIndex คืน index ของ array ผ่าน parameter out แยกต่างหาก และคืน offset เต็มของ entry เป็นผลลัพธ์ ค่าทั้งสองจึงไม่ถูกตัดทอน release เดียวกันยังถี่ถ้วนเพื่อนบ้านอีกสองตัว KeyName ตอนนี้นับและคืนแต่ key ที่เป็น string จริง ๆ และคืน string ว่างสำหรับ index 0 หรือต่ำกว่า แทนที่จะ cast object อะไรก็ได้ที่ตามหลัง key ที่ไม่ถูกต้อง HasKey ไม่ถือ key ที่เป็นตัวเลขหรือไม่ถูกต้องเป็นชื่อว่างอีก สำหรับ leaf อย่าง [(Valid) 42 123 456] HasKey('') ตอนนี้เป็น False และ KeyName(2) คืน string ว่าง

procedure AuditTrees(const FileName: string);
var
  Lib: TPDFlib;
  I: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile(FileName, '') <> 1 then
      Exit;
    // number tree ของ /PageLabels; ไฟล์ที่ไม่มีจะคืนเลขหน้าธรรมดา
    for I := 1 to Lib.PageCount do
      WriteLn('Page ', I, ' label: ', Lib.GetPageLabel(I));
    // name tree ของ /EmbeddedFiles; index เริ่มที่ 1, key ที่ไม่ใช่ string ถูกข้าม
    for I := 1 to Lib.EmbeddedFileCount do
      WriteLn('Attachment ', I, ': ', Lib.GetEmbeddedFileStrProperty(I, 1),
        ' (', Lib.GetEmbeddedFileStrProperty(I, 2), ')');  // ชื่อ, MIME type
    // name tree ของ /JavaScript: แค่ลิสต์ชื่อ package ไม่ execute อะไร
    for I := 1 to Lib.GlobalJavaScriptCount do
      WriteLn('Script package: ', Lib.GlobalJavaScriptPackageName(I));
  finally
    Lib.Free;
  end;
end;

บนไฟล์ที่ปั้นเองไฟล์เดียวกัน ที่รากของ /PageLabels ลิสต์ leaf ตัวหนึ่งซ้ำสองครั้งและอ้างถึงตัวเอง การ audit นี้พิมพ์ i กับ A-1 ให้สองหน้า แต่ละ range ครั้งเดียว และ script package ตัวเดียวจาก tree /JavaScript ที่ก็ชี้กลับไปหารากของมันเช่นกัน ฝั่งเขียนของ page label มีประวัติกับรากแบบ /Kids ของตัวเอง เล่าไว้ในการแก้ page label ของ PDF ที่เก็บใน number tree แบบ /Kids AddPageLabels ทำให้รากแบบนั้นแบนก่อนแทรก และมันอาศัยการนับแบบ EnumNumTree ที่เล่าไว้ในบทความนี้

การแข็งแรงนี้ยังไม่การันตีอะไรบ้าง

การแข็งแรงนี้การันตีการจบลง ลำดับที่นิ่ง และผลที่ถูกต้องสำหรับ tree ที่ key จริงยังสมบูรณ์ มันไม่ได้ทำให้ tree ที่พังหมายถึงสิ่งที่ผู้เขียนตั้งใจ มีขีดจำกัดหลายข้อที่ควรรู้ก่อนจะสร้างต่อบนมัน

  • visited set ทำงานด้วย identity ของ object dictionary สองตัวที่เนื้อหาเหมือนกันเป๊ะคือสอง node สำเนา leaf ที่ producer เอามาแปะแทนการอ้างอิงจึงยังให้ entry ซ้ำ
  • /Limits ที่สมบูรณ์ เรียงถูกทิศ แต่ผิดค่า ยังตัดกิ่งได้อยู่ดี reader ที่ใช้ range เป็น optimization ไม่อาจภูมิคุ้มกัน range ที่โกหกอย่างน่าเชื่อได้ทางเดียวทางเลือกคือละทิ้ง /Limits ทั้งหมดแล้วสแกนทุก leaf
  • การนับคงลำดับไฟล์แต่ไม่เรียงให้ GetPageLabel ใช้ range สุดท้ายที่นับได้ซึ่งอยู่ที่หรือต่ำกว่าหน้า producer ที่เขียน range พล่านลำดับจึงได้ semantics ตามลำดับไฟล์
  • หน่วยความจำโตตามจำนวน node กับ entry ที่ต่างกัน การเดินเพิ่มแค่ list กับ hash set เท่านั้น แต่ name tree ขนาด 100 MB ก็ยังเป็น name tree ขนาด 100 MB หลัง parse
  • key ซ้ำข้างใน leaf เดียวไม่ถูกรายงาน binary search คืนคู่ที่มันชนก่อนเท่านั้น ส่วน linear fallback คง match สุดท้ายที่มันสแกนเจอ

สรุปด่วน: อ่าน tree ของ PDF จากไฟล์ที่ไม่น่าเชื่อถือ

  • อัปเกรดเป็น v3.539.45 ขึ้นไปเพื่อการเดิน name tree กับ number tree ที่ทน cycle ทน stack และเป็น v3.539.51 ขึ้นไปเพื่อให้ /Limits ที่กลับด้านไม่ซ่อน key
  • ถือว่า GetNamedDestination คืน 0 หมายถึง "ไม่มี" และ GetDestPage คืน 0 หมายถึง "มีแต่ใช้ไม่ได้"
  • ใช้ GlobalJavaScriptCount กับ GlobalJavaScriptPackageName กับ name tree ของ /JavaScript ส่วน GetDocJavaScript อ่าน trigger ของ /AA ใน catalog แทน
  • index attachment กับ script package จาก 1 ถึงจำนวนที่ library รายงาน key ที่ไม่ถูกต้องไม่ถูกนับ
  • ในโค้ด tree ของคุณเอง mark node ว่าเห็นแล้วตอน pop push ลูกย้อนทาง และปล่อยให้ /Limits ตัดกิ่งเฉพาะเมื่อมันเป็นคู่ชนิดถูกและเรียงถูกทิศ

เครื่องมือ pre-flight, archiver และ viewer ต่างอ่าน tree พวกนี้ก่อนหน้าใดจะถูก render จึงต้องรอดจากทุกอย่างที่ไหลเข้าคิวอัปโหลด ตัวอ่าน tree ที่เล่าไว้ข้างบนมาพร้อมPDFlibPas, the PDF Library for Delphi ซึ่ง build ได้ทั้งกับ Delphi และ Free Pascal