Τεχνικό Άρθρο

PDFlibPas name trees: κύκλοι, κακά Limits, τεράστια leaves

Το PDFlibPas, η PDF Library της losLab για Delphi, διασχίζει name trees και number trees PDF με ρητή στοίβα και σύνολο επισκεφθέντων από το v3.539.45, οπότε κυκλικά /Kids, κοινά παιδιά και δέντρα χιλιάδων επιπέδων δεν εξαντλούν πια τη στοίβα κλήσεων ούτε αντιγράφουν εγγραφές. Από το v3.539.51 ένα λείπον, κακοδιατυπωμένο ή αντεστραμμένο ζεύγος /Limits δεν κρύβει ποτέ κλάδο που κρατά το κλειδί. Named destinations, page labels, συνημμένα και JavaScript επιπέδου εγγράφου διαβάζουν όλα μέσα από αυτές τις δύο διαδρομές κώδικα, που τις κάνει μέρος της επιφάνειας επίθεσης κάθε PDF που δεν παράγατε εσείς

Το έναυσμα σπάνια είναι εξωτικό. Ένα fuzzer, μια εχθρική αποστολή ή ένα buggy incremental save γράφει εγγραφή /Kids που δείχνει πίσω σε πρόγονο, και ένας αναδρομικός περιπατητής πεθαίνει με stack overflow σε αρχείο δύο kilobyte. Η πιο αθόρυβη αποτυχία είναι μια αναζήτηση που εμπιστεύεται χαλασμένο πίνακα /Limits και αναφέρει "not found" για ένα destination που είναι φανερά εκεί

Πού εμφανίζονται name trees και number trees σε ένα PDF;

Name trees και number trees εμφανίζονται όπου ένα PDF αντιστοιχίζει μεγάλο σύνολο κλειδιών σε objects, και το PDFlibPas διαβάζει τουλάχιστον τέσσερα από αυτά μέσω δημόσιων APIs. Το ISO 32000-1 §7.9.6 ορίζει το name tree (string κλειδιά, Table 36) και το §7.9.7 το number tree (ακεραία κλειδιά, Table 37). Και τα δύο είναι σχεδόν ισορροπημένα δέντρα των οποίων η ρίζα και οι ενδιάμεσοι κόμβοι κρατούν /Kids, των οποίων τα leaves κρατούν τα ταξινομημένα ζεύγη κλειδί/τιμή σε /Names ή /Nums, και των οποίων οι μη ριζικοί κόμβοι κρατούν πίνακα /Limits δύο στοιχείων με το μικρότερο και μεγαλύτερο κλειδί από κάτω τους

ΔέντροΠού κατοικείΠροδιαγραφήAPI ανάγνωσης PDFlibPas
Named destinations/Dests στο name dictionary§12.3.2.3GetNamedDestination, μετά GetDestPage / GetDestType
Page labels/PageLabels στον κατάλογο (number tree)§12.4.2GetPageLabel
Συνημμένα/EmbeddedFiles στο name dictionary§7.7.4, §7.11.4EmbeddedFileCount, GetEmbeddedFileStrProperty
JavaScript επιπέδου εγγράφου/JavaScript στο name dictionary§7.7.4GlobalJavaScriptCount, GlobalJavaScriptPackageName

Δύο λεπτομέρειες σε εκείνον τον πίνακα περνούν εύκολα απαρατήρητες. Τα named destinations έχουν επίσης μια παλαιότερη μορφή PDF 1.1, ένα σκέτο λεξικό /Dests στον κατάλογο κλειδωμένο με name objects, και το GetNamedDestination ελέγχει πρώτα εκείνο το λεξικό πριν κατέβει το name tree του PDF 1.2. Και το GetDocJavaScript δεν είναι καθόλου αναγνώστης name tree: επιστρέφει τα scripts συνδεδεμένα σε triggers εγγράφου στο λεξικό /AA του καταλόγου (WS, DS, WP, DP, DC), ενώ τα ονομασμένα script packages που τρέχουν όταν ανοίγει ένα έγγραφο ζουν στο name tree /JavaScript

Κάθε byte εκείνων των δομών προέρχεται από το αρχείο. Η προδιαγραφή λέει τι πρέπει να παράγει ένας writer· δεν μπορεί να εμποδίσει έναν reader να λάβει κάτι άλλο, που είναι το ίδιο μάθημα πίσω από το hardening ενός Pascal PDF parser απέναντι σε κακόβουλα αρχεία, εφαρμοσμένο εδώ στο σχήμα του δέντρου αντί για μεγέθη buffers

Γιατί ένας κυκλικός πίνακας /Kids καταρρέει αναδρομικό περιπατητή δέντρου;

Ένας κυκλικός πίνακας /Kids καταρρέει αναδρομικό περιπατητή επειδή τίποτα στην αναδρομή δεν προσέχει ότι έχει ξαναδεί έναν κόμβο, οπότε ένα παιδί που αναφέρεται στον δικό του πρόγονο γυρνά ένα πεπερασμένο αρχείο σε άπειρη κατάβαση. Πριν το v3.539.45, τα NameTreeLookup, NumTreeLookup, EnumNumTree και το εσωτερικό TPDFNameTree.ProcessNode καλούσαν όλα τον εαυτό τους μία φορά ανά παιδί. Μία αυτοαναφορά αρκούσε για να τελειώσει η διεργασία, και ένα νόμιμο αλλά πολύ βαθύ δέντρο μπορούσε να κάνει το ίδιο χωρίς καθόλου κύκλο

Μια ηπιότερη παραλλαγή χαλάει αποτελέσματα αντί να καταρρέει. Όταν δύο εγγραφές /Kids αναφέρονται στο ίδιο leaf, μια αφελής απαρίθμηση το επισκέπτεται δύο φορές, και ένα πλήθος συνημμένων ή μια λίστα script packages αναφέρει εγγραφές που δεν υπάρχουν

Η διόρθωση αντικαθιστά την αναδρομή με ρητή στοίβα last-in, first-out στον heap και σύνολο επισκεφθέντων κλειδωμένο στην ταυτότητα λεξικών. Ένας κόμβος σημειώνεται όταν αναδύεται, όχι όταν ωθείται, οπότε μια κυκλική reference μπορεί να καθίσει στη στοίβα για λίγο αλλά απορρίπτεται τη στιγμή που ξαναβγαίνει πάνω. Κάθε ξεχωριστός κόμβος αναπτύσσει τα παιδιά του ακριβώς μία φορά, που οριοθετεί τη συνολική δουλειά από το πλήθος των ξεχωριστών λεξικών συν το συνολικό μήκος των πινάκων /Kids τους. Το βάθος παύει να μετράει: μια αλυσίδα 4.096 επιπέδων είναι απλώς 4.096 επαναλήψεις ενός βρόχου και 4.096 εγγραφές σε ένα hash set

Διάσχιση name tree PDFlibPas όπου πίνακας Kid που γύριζε πίσω στη ρίζα σκότωσε αναδρομικό περιπατητή με stack overflow, αντικατεστημένος από το v3.539.45 με ρητή στοίβα και σύνολο επισκεφθέντων που σημειώνει κόμβους στο pop, ωθεί παιδιά από δεξιά προς αριστερά και κρατά leaves σε σειρά αρχείου για το GetPageLabel
Το βάθος παύει να μετράει όταν η αναδρομή γίνεται βρόχος: μια αλυσίδα 4.096 επιπέδων είναι απλώς 4.096 επαναλήψεις και 4.096 εγγραφές hash set

Η σειρά εξακολουθεί όμως να μετράει, και η στοίβα πρέπει να τροφοδοτείται ανάποδα για να τη κρατήσει. Τα παιδιά ωθούνται από τον τελευταίο δείκτη προς τον πρώτο, οπότε το αριστερότερο παιδί αναδύεται πρώτο και τα leaves βγαίνουν στην ίδια σειρά αριστερά-προς-δεξιά που έγραψε ο παραγωγός. Το GetPageLabel εξαρτάται από αυτό: διασχίζει κάθε απαριθμημένο εύρος και εφαρμόζει το τελευταίο του οποίου ο δείκτης εκκίνησης είναι στο ή κάτω από τη σελίδα, οπότε η αντιστροφή της απαρίθμησης θα παραδίδει αθόρυβα στη σελίδα 200 το στυλ των προμελετών. Ο σκελετός παρακάτω δείχνει το μοτίβο πάνω σε αφηρημένο τύπο κόμβου, ανεξάρτητο από οποιοδήποτε μοντέλο PDF objects

uses
  System.Generics.Collections;

type
  TTreeNode = class
  public
    Kids: TArray<TTreeNode>;   // κενό σε leaf
    Keys: TArray<string>;      // κλειδιά 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;                      // κύκλος ή κοινό παιδί: το έχουμε δει
      Visited.Add(Node, 0);
      if Length(Node.Kids) > 0 then
      begin
        // Ωθήση από δεξιά προς αριστερά ώστε το αριστερότερο kid να αναδύεται πρώτο
        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 δεν είναι ετυμηγορία: συνεχίστε την ανάδυση αδελφών
    end;
  finally
    Visited.Free;
    Pending.Free;
  end;
end;

Γιατί δεν μπορεί μια αναζήτηση να σταματήσει στον πρώτο ταιριάζοντα κλάδο;

Μια αναζήτηση δεν μπορεί να σταματήσει στον πρώτο κλάδο του οποίου το εύρος ταιριάζει, επειδή τα εύρη /Limits σε πραγματικό αρχείο μπορούν να αλληλοκαλύπτονται ή να λένε ψέματα, και ο κλάδος που διεκδικεί το κλειδί δεν είναι απαραίτητα ο κλάδος που το κρατά. Οι αναζητήσεις πριν το v3.539.45 έθεταν flag Found στο πρώτο παιδί του οποίου τα /Limits κάλυπταν το κλειδί, κατέβαιναν σε αυτό, και δεν κοίταζαν ποτέ άλλο αδελφό. Αν εκείνο το παιδί αποδεικνυόταν κενό, παρωχημένο ή βρόχος πίσω στη ρίζα, η απάντηση ήταν nil, ακόμα κι όταν το αμέσως επόμενο αδελφό κρατούσε το κλειδί

Το ξαναγραμμένο FindTreeValue, που πλέον στηρίζει και τα NameTreeLookup και NumTreeLookup, ωθεί κάθε παιδί του οποίου το εύρος δεν αποκλείει το κλειδί και συνεχίζει την ανάδυση μέχρι να βρει ταίριασμα ή να αδειάσει η στοίβα. Μια αστοχία μέσα σε ένα leaf είναι απλώς αστοχία μέσα σε ένα leaf. Σε καλοδιατυπωμένο δέντρο αυτό δεν κοστίζει τίποτα extra· σε κατεστραμμένο κοστίζει μερικές ακόμα επισκέψεις κόμβων και επιστρέφει τη σωστή απάντηση

Η αναζήτηση leaf ακολουθεί την ίδια φιλοσοφία. Το ISO 32000-1 απαιτεί τα κλειδιά σε πίνακα /Names να είναι ταξινομημένα κατά τιμή byte, οπότε το leaf ψάχνεται πρώτα με binary search. Αν αποτύχει, το PDFlibPas υποχωρεί σε γραμμική σάρωση των ζευγών, επειδή ένα leaf εκτός σειράς θα έκανε αλλιώς παρόν κλειδί αόρατο. Η ταξινόμηση είναι γρήγορη διαδρομή, όχι φίλτρο

Η αναζήτηση αρνείται επίσης να μαντέψει πάνω σε μία δομική αντίφαση. Το Table 36 επιτρέπει σε έναν κόμβο να κρατά είτε /Kids είτε /Names, ποτέ και τα δύο, και η διαδρομή αναζήτησης μεταχειρίζεται κόμβο που κρατά και τα δύο ως κακοδιατυπωμένο και τον παραλείπει αντί να διαλέξει μία ερμηνεία. Οι διαδρομές απαρίθμησης όπως το EnumNumTree είναι πιο επιεικές και ακολουθούν /Kids όταν και τα δύο υπάρχουν

Σε τι επιτρέπεται να εμπιστεύεται ένας reader τα /Limits;

Ένας reader επιτρέπεται να εμπιστεύεται τα /Limits μόνο για να παραλείπει δουλειά, ποτέ για να αποφανθεί ότι ένα κλειδί απουσιάζει, και μόνο όταν το ζεύγος είναι καλοδιατυπωμένο. Το Table 36 λέει ότι ενδιάμεσοι και leaf κόμβοι πρέπει να κρατούν /Limits ως πίνακα δύο στοιχείων του ελάχιστου και μέγιστου κλειδιού, αλλά στην πράξη η εγγραφή χάνεται μετά από χειροκίνητες αλλαγές, κρατά αριθμούς σε name tree, ή φτάνει με αντεστραμμένα όρια. Το PDFlibPas v3.539.45 και το v3.539.51 λύνουν κάθε περίπτωση με τον ίδιο τρόπο: αν το εύρος δεν μπορεί να διαβαστεί ως διατεταγμένο ζεύγος του σωστού τύπου, το παιδί μένει αναζητήσιμο

  • Λείποντα /Limits: ο παλιός έλεγχος εύρους επέστρεφε False και το παιδί παραλειπόταν ολότελα, οπότε ένας producer που ξέχασε την εγγραφή έκανε όλο το subtree του απρόσιτο. Από το v3.539.45 το παιδί αναζητείται
  • Λάθος τύπος ή λάθος μήκος, όπως αριθμοί σε name tree ή πίνακας ενός στοιχείου: μεταχειρίζεται ακριβώς όπως λείπουσα εγγραφή από το v3.539.45
  • Αντεστραμμένα όρια όπως [(Z) (A)] ή [9 0]: το v3.539.45 τα χρησιμοποιούσε ακόμα, και κανένα κλειδί δεν μπορεί να ικανοποιήσει Lo <= Key <= Hi όταν Lo > Hi, οπότε ο κλάδος αποκλειόταν για κάθε αναζήτηση. Από το v3.539.51 ένα εύρος χρησιμοποιείται για κλάδεμα μόνο όταν το κάτω όριο του δεν υπερβαίνει το πάνω
  • Καλοδιατυπωμένο, διατεταγμένο και σωστό: χρησιμοποιείται για να παραλειφθεί ο κλάδος, που είναι και όλο το νόημα της εγγραφής
Κανόνες PDFlibPas για εμπιστοσύνη σε πίνακα Limits name tree: λείπον, λάθος τύπου ή αντεστραμμένο ζεύγος αφήνει το παιδί αναζητήσιμο από τα v3.539.45 και v3.539.51, και μόνο καλοδιατυπωμένο διατεταγμένο ζεύγος επιτρέπεται να κλαδέψει τον κλάδο, ώστε εχθρικά Limits να κοστίζουν επισκέψεις αλλά να μην κρύβουν πια υπάρχον destination
Τα εύρη μπορούν να παραλείπουν δουλειά αλλά ποτέ να αποφανθούν απουσία, επειδή τα πραγματικά κλειδιά αποθηκευμένα στα leaves αποφασίζουν το αποτέλεσμα κάθε αναζήτησης

Τα πραγματικά κλειδιά αποφασίζουν το αποτέλεσμα σε κάθε περίπτωση. Ένα εχθρικό /Limits μπορεί να κάνει το PDFlibPas να επισκεφθεί περισσότερους κόμβους από όσους χρειάζεται, αλλά ένα κακοδιατυπωμένο δεν μπορεί πια να κάνει υπάρχον destination να εξαφανιστεί. Από την πλευρά του καλούντος τίποτα δεν αλλάζει: το 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;
    // Πρώτα το /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 έχει ένα παιδί που γυρίζει πίσω στη ρίζα κάτω από εύρος [(a) (z)] και ένα δεύτερο παιδί που κρατά την πραγματική εγγραφή κάτω από αντεστραμμένα όρια [(z) (a)], η διαδικασία αυτή λύνει το destination στη σελίδα 2 με τύπο προβολής 2 (Fit). Πριν το v3.539.45 η ίδια αναζήτηση επέστρεφε 0, επειδή το παιδί-βρόχος διεκδικούσε το κλειδί πρώτο και η αναζήτηση δεν έφτανε ποτέ στον αδελφό του· το v3.539.45 μόνο του επέστρεφε ακόμα 0, επειδή το αντεστραμμένο εύρος απέκλειε το πραγματικό leaf. Αν μετά διαβάσετε το outline που δείχνει σε αυτά τα destinations, το συνοδευτικό άρθρο για την ανάγνωση ενεργειών PDF bookmark και annotation σε Delphi καλύπτει την πλευρά ενεργειών

Πώς έσπασε σε ένα TPDFNameTree ένα leaf με 32.769 names;

Ένα leaf με 32.769 ζεύγη name/value έσπασε το TPDFNameTree επειδή το εσωτερικό του FindIndex πακετάριζε δύο αριθμούς σε έναν 32-bit Integer: τη θέση του leaf στην εσωτερική λίστα πινάκων στα υψηλά 16 bits και το offset εγγραφής μέσα στον πίνακα /Names εκείνου του leaf στα χαμηλά 16 bits. Κάθε ζεύγος καταλαμβάνει δύο θέσεις πίνακα, οπότε το 32.769ο ζεύγος, δείκτης ζεύγους 32.768, αρχίζει στο offset 65.536, που είναι $10000. Εκείνη η τιμή μεταφέρει στο υψηλό μισό, και ο decoder τη διάβαζε πίσω ως offset 0 στο επόμενο leaf

Πακετάρισμα FindIndex του TPDFNameTree στο PDFlibPas όπου θέση leaf και offset εγγραφής μοιράζονταν έναν 32-bit Integer και το ζεύγος 32768 άρχιζε στο offset 65536, οπότε η μεταφορά στο υψηλό μισό διαβαζόταν ως offset 0 του επόμενου leaf και τα FindKey ή DeleteKey αγγίζαν λάθος ζεύγος ενώ το HasKey διαφωνούσε
Δύο 16-bit τιμές σε έναν 32-bit ακέραιο κόβουν αθόρυβα τη στιγμή που ένα leaf ξεπερνά 32.768 ζεύγη, μέγεθος που πραγματικά reference manuals φτάνουν

Το TPDFNameTree είναι η κλάση πίσω από συνημμένα, global JavaScript packages και εγγραφές named destinations, που κάνει τις συνέπειες συγκεκριμένες. Σε δέντρο ενός leaf δεν υπάρχει επόμενο leaf, οπότε τα FindKey και DeleteKey δεικτοδοτούσαν μετά το τέλος της λίστας leaves· σε δέντρο πολλών leaf επέστρεφαν ή διέγραφαν το πρώτο ζεύγος του επόμενου leaf αντί για το ζητούμενο. Εν τω μεταξύ το HasKey έτρεχε τη δική του σάρωση και ανέφερε το κλειδί ως παρόν, οπότε η κλάση αλληλοσυγκρουόταν. Ένα παραγόμενο reference manual με ένα named destination ανά σύμβολο API ξεπερνά 32.768 εγγραφές χωρίς να προσπαθεί, και μερικοί producers γράφουν όλα μέσα σε ένα επίπεδο leaf

Από το v3.539.45, το FindIndex επιστρέφει τον δείκτη πίνακα μέσω ξεχωριστής παραμέτρου out και το πλήρες offset εγγραφής ως αποτέλεσμά του, οπότε καμία τιμή δεν κόβεται. Η ίδια έκδοση σφίγγει δύο γείτονες. Το KeyName μετρά και επιστρέφει πλέον μόνο γνήσια string κλειδιά και επιστρέφει κενή συμβολοσειρά για δείκτη 0 ή κάτω, όπου πριν μεταχειριζόταν με cast ό,τι object ακολουθούσε ένα άκυρο κλειδί. Το HasKey δεν μεταχειρίζεται πια αριθμητικό ή αλλιώς άκυρο κλειδί ως κενό όνομα. Για leaf όπως [(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 tree /PageLabels· αρχεία χωρίς ένα επιστρέφουν σκέτους αριθμούς σελίδων
    for I := 1 to Lib.PageCount do
      WriteLn('Page ', I, ' label: ', Lib.GetPageLabel(I));
    // Name tree /EmbeddedFiles· οι δείκτες ξεκινούν από 1, μη string κλειδιά παραλείπονται
    for I := 1 to Lib.EmbeddedFileCount do
      WriteLn('Attachment ', I, ': ', Lib.GetEmbeddedFileStrProperty(I, 1),
        ' (', Lib.GetEmbeddedFileStrProperty(I, 2), ')');  // όνομα, τύπος MIME
    // Name tree /JavaScript: λίστα ονομάτων packages, καμία εκτέλεση
    for I := 1 to Lib.GlobalJavaScriptCount do
      WriteLn('Script package: ', Lib.GlobalJavaScriptPackageName(I));
  finally
    Lib.Free;
  end;
end;

Στο ίδιο χειροποίητο αρχείο, του οποίου η ρίζα /PageLabels απαριθμεί ένα leaf δύο φορές και αναφέρεται στον εαυτό της, αυτός ο έλεγχος τυπώνει i και A-1 για τις δύο σελίδες, κάθε εύρος μία φορά, και το μοναδικό script package από δέντρο /JavaScript που επίσης δείχνει πίσω στη δική του ρίζα. Η πλευρά εγγραφής των page labels έχει τη δική της ιστορία με ρίζες /Kids, καλυμμένη στο διόρθωση PDF page labels αποθηκευμένων σε number trees /Kids· το AddPageLabels ισιώνει τέτοια ρίζα πριν την εισαγωγή, και βασίζεται στην ίδια απαρίθμηση EnumNumTree που περιγράφεται εδώ

Τι δεν εγγυάται ακόμα αυτό το hardening;

Το hardening εγγυάται τερματισμό, σταθερή σειρά και σωστά αποτελέσματα για δέντρα των οποίων τα πραγματικά κλειδιά είναι ανέπαφα· δεν κάνει ένα κατεστραμμένο δέντρο να σημαίνει αυτό που εννοούσε ο συγγραφέας του. Μερικά όρια αξίζει να ξέρετε πριν χτίσετε πάνω του

  • Το σύνολο επισκεφθέντων δουλεύει με ταυτότητα objects. Δύο ξεχωριστά λεξικά με πανομοιότυπο περιεχόμενο είναι δύο κόμβοι, οπότε ένας producer που αντιγράφει ένα leaf αντί να το αναφέρει εξακολουθεί να παράγει διπλές εγγραφές
  • Καλοδιατυπωμένο, διατεταγμένο αλλά λάθος /Limits κλαδεύει ακόμα. Ένας reader που χρησιμοποιεί εύρη ως βελτιστοποίηση δεν μπορεί επίσης να είναι απρόσβλητος σε εύρος που λέει πειστικά ψέματα· η μοναδική εναλλακτική είναι να αγνοήσει εντελώς τα /Limits και να σαράρει κάθε leaf
  • Η απαρίθμηση διατηρεί τη σειρά αρχείου αλλά δεν ταξινομεί. Το GetPageLabel εφαρμόζει το τελευταίο απαριθμημένο εύρος στο ή κάτω από τη σελίδα, οπότε ένας producer που γράφει εύρη εκτός σειράς παίρνει σημασιολογία σειράς αρχείου
  • Η μνήμη μεγαλώνει με το πλήθος των ξεχωριστών κόμβων και εγγραφών. Η διάσχιση προσθέτει μια λίστα και ένα hash set, τίποτα άλλο, αλλά ένα name tree 100 MB εξακολουθεί να είναι name tree 100 MB μετά το parsing
  • Διπλά κλειδιά μέσα σε ένα leaf δεν αναφέρονται. Η binary search επιστρέφει ό,τι ταιριάζον ζεύγος συναντήσει πρώτο· η γραμμική εναλλακτική κρατά το τελευταίο ταίριασμα που σαράρει

Σύντομη αναφορά: ανάγνωση PDF δέντρων από αναξιόπιστα αρχεία

  • Αναβαθμίστε σε v3.539.45 ή νεότερο για διάσχιση name trees και number trees ασφαλή σε κύκλους και στοίβα, και σε v3.539.51 ή νεότερο ώστε αντεστραμμένα /Limits να μην κρύβουν πια κλειδιά
  • Μεταχειριστείτε επιστροφή 0 από GetNamedDestination ως "απουσιάζει", και επιστροφή 0 από GetDestPage ως "υπάρχει αλλά δεν χρησιμοποιείται"
  • Χρησιμοποιήστε GlobalJavaScriptCount και GlobalJavaScriptPackageName για το name tree /JavaScript· το GetDocJavaScript διαβάζει αντ' αυτού triggers /AA του καταλόγου
  • Δεικτοδοτήστε συνημμένα και script packages από 1 έως το πλήθος που αναφέρει η βιβλιοθήκη· άκυρα κλειδιά δεν μετρώνται
  • Στον δικό σας κώδικα δέντρων, σημειώστε κόμβους ως επισκεφθέντες στο pop, ωθήστε παιδιά ανάστροφα, και αφήστε τα /Limits να κλαδεύουν μόνο όταν είναι καλοτυπωμένο, διατεταγμένο ζεύγος

Προπτήξια εργαλεία, archivers και viewers διαβάζουν αυτά τα δέντρα πριν αποδοθεί οποιαδήποτε σελίδα, οπότε πρέπει να επιβιώνουν από ό,τι φτάνει σε μια ουρά αποστολών. Οι αναγνώστες δέντρων που περιγράφονται παραπάνω έρχονται με το PDFlibPas, τη PDF Library για Delphi, που χτίζει τόσο με Delphi όσο και με Free Pascal