Το 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.3 | GetNamedDestination, μετά GetDestPage / GetDestType |
| Page labels | /PageLabels στον κατάλογο (number tree) | §12.4.2 | GetPageLabel |
| Συνημμένα | /EmbeddedFiles στο name dictionary | §7.7.4, §7.11.4 | EmbeddedFileCount, GetEmbeddedFileStrProperty |
| JavaScript επιπέδου εγγράφου | /JavaScript στο name dictionary | §7.7.4 | GlobalJavaScriptCount, 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
Η σειρά εξακολουθεί όμως να μετράει, και η στοίβα πρέπει να τροφοδοτείται ανάποδα για να τη κρατήσει. Τα παιδιά ωθούνται από τον τελευταίο δείκτη προς τον πρώτο, οπότε το αριστερότερο παιδί αναδύεται πρώτο και τα 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 ένα εύρος χρησιμοποιείται για κλάδεμα μόνο όταν το κάτω όριο του δεν υπερβαίνει το πάνω - Καλοδιατυπωμένο, διατεταγμένο και σωστό: χρησιμοποιείται για να παραλειφθεί ο κλάδος, που είναι και όλο το νόημα της εγγραφής
Τα πραγματικά κλειδιά αποφασίζουν το αποτέλεσμα σε κάθε περίπτωση. Ένα εχθρικό /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
Το 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