Τα σφάλματα ελέγχου εύρους (range check errors) στις βιβλιοθήκες PDF Delphi αποκτούν τη φήμη ότι είναι δύσκολο να εντοπιστούν επειδή δεν ακολουθούν ένα συνεπές μοτίβο εισόδου. Το ίδιο έγγραφο τα παράγει σε ένα μηχάνημα και όχι σε ένα άλλο. η ίδια διαδρομή κώδικα πυροδοτεί την εξαίρεση σε ένα αρχείο 3 σελίδων, αλλά εκτελείται καθαρά σε ένα αρχείο 12 σελίδων. Αυτή η ασυνέπεια ανάγεται σχεδόν πάντα σε μία βασική αιτία: τα αντικείμενα σελίδας PDF δεν αποθηκεύονται με σειρά αρχείου. Εάν η βιβλιοθήκη κατασκευάζει τον εσωτερικό της πίνακα σελίδων σαρώνοντας τα αντικείμενα διαδοχικά αντί να περπατά στο δέντρο σελίδων που δηλώνεται από τον κατάλογο, κατασκευάζει ένα ευρετήριο του οποίου το έγκυρο εύρος δεν ταιριάζει με αυτό που περιμένουν οι καλούντες, και ο έλεγχος εύρους εντοπίζει αυτή την αναντιστοιχία τη χειρότερη δυνατή στιγμή
Πώς λειτουργεί ο έλεγχος εύρους στο Delphi
Με ενεργή την οδηγία μεταγλωττιστή {$R+} (η προεπιλογή στη διαμόρφωση Debug), το Delphi RTL επικυρώνει κάθε δείκτη πίνακα, δείκτη συμβολοσειράς και αντιστοίχιση απαρίθμησης (enumerated assignment) κατά το χρόνο εκτέλεσης (runtime). Μια πρόσβαση εκτός ορίων (out-of-bounds) εγείρει ERangeError αντί να διαβάζει αθόρυβα την παρακείμενη μνήμη. Αυτή η συμπεριφορά είναι πολύτιμη: αναδεικνύει λανθάνοντα σφάλματα έγκαιρα αντί να τα αφήνει να καταστρέψουν μια δομή δεδομένων που αποτυγχάνει μόνο εκατό γραμμές αργότερα. Το απογοητευτικό μέρος είναι ότι η εξαίρεση ενεργοποιείται στον ιστότοπο πρόσβασης, όχι στο σημείο όπου το ευρετήριο υπολογίστηκε εσφαλμένα. Όταν η στοίβα κλήσεων (call stack) δείχνει μια βαθιά ένθετη μέθοδο σε μια μονάδα PDF, το πραγματικό λάθος είναι συνήθως αρκετά πλαίσια (frames) πιο πίσω
Οι σύνθετες συνθήκες boolean το κάνουν αυτό χειρότερο. Το Delphi αξιολογεί τις εκφράσεις and από αριστερά προς τα δεξιά με σημασιολογία βραχυκυκλώματος (short-circuit), αλλά το βραχυκύκλωμα παραλείπει την αξιολόγηση μόνο όταν η αριστερή πλευρά είναι False. Μια έκφραση όπως:
if FDocStarted and (DestIndex < Length(PageArr)) and
(PageArr[DestIndex].PageObj <> nil) then
φαίνεται ασφαλής, αλλά προστατεύει από ένα ευρετήριο εκτός εύρους μόνο εάν το FDocStarted είναι True και το DestIndex είναι μη αρνητικό. Ο έλεγχος DestIndex < Length(PageArr) δεν κάνει τίποτα όταν το DestIndex είναι αρνητικό, επειδή η σύγκριση ενός αρνητικού ακεραίου με ένα μη αρνητικό μήκος επιστρέφει True στην αριθμητική με πρόσημο (signed arithmetic) και η επακόλουθη πρόσβαση στον πίνακα εξακολουθεί να πυροδοτεί το σφάλμα εύρους. Η μετακίνηση του ελέγχου ορίων (bounds check) στην πιο εξωτερική θέση είναι η σωστή διόρθωση:
if (DestIndex >= 0) and (DestIndex < Length(PageArr)) then
begin
if FDocStarted and (PageArr[DestIndex].PageObj <> nil) then
Result := PageArr[DestIndex].PageObj
else
Result := nil;
end
else
raise ERangeError.CreateFmt(
'Page index %d is out of range (0..%d)',
[DestIndex, Length(PageArr) - 1]);
Αυτή είναι η μηχανική διόρθωση. Σταματά την κατάρρευση (crash). Δεν εξηγεί γιατί το DestIndex έλαβε αρχικά μια τιμή εκτός του έγκυρου εύρους
Η πραγματική αιτία: σειρά αντικειμένων έναντι σειράς σελίδων
Το ISO 32000-1 §7.7.3 ορίζει το δέντρο σελίδων ως ένα δέντρο κόμβων Pages των οποίων οι πίνακες Kids παραθέτουν αντικείμενα σελίδας με σειρά εμφάνισης. Το αρχείο αποθηκεύει αυτά τα αντικείμενα σε όποιες μετατοπίσεις (offsets) έτυχε να επιλέξει ο συγγραφέας (writer). Το αντικείμενο νούμερο 20 μπορεί φυσικά να προηγείται του αντικειμένου νούμερο 3 στη ροή byte (byte stream). Μια βιβλιοθήκη που δημιουργεί τη λίστα σελίδων της επαναλαμβάνοντας τον πίνακα παραπομπών (cross-reference table) με σειρά αριθμών αντικειμένων, αντί να ακολουθεί την αλυσίδα Kids, θα παράγει μια ακολουθία που αποκλίνει από αυτό που περιμένει ο χρήστης. Σε έγγραφα όπου η γεννήτρια (generator) έτυχε να γράψει σελίδες με τη σειρά, όλα λειτουργούν. Σε έγγραφα όπου δεν το έκανε, η ασυμφωνία μεταξύ της αρίθμησης σελίδων της βιβλιοθήκης και της αρίθμησης σελίδων του καλούντος (caller) παράγει ευρετήρια που βρίσκονται εκτός του PageArr
Η σωστή προσέγγιση είναι να ξεκινήσετε από τον κατάλογο (catalog), να επιλύσετε την έμμεση αναφορά /Pages και να περπατήσετε (walk) τον πίνακα Kids αναδρομικά. Για ένα επίπεδο έγγραφο χωρίς ενδιάμεσους κόμβους Pages, η διέλευση είναι απλή:
procedure BuildPageIndexFromTree(
const KidsArray: THPDFArray;
var PageArr: TPageObjArray);
var
i, Idx: Integer;
Child: THPDFObject;
ChildType: string;
begin
for i := 0 to KidsArray.Count - 1 do
begin
Child := KidsArray.GetIndirectObject(i);
if Child = nil then
Continue;
ChildType := Child.GetNameValue('/Type');
if ChildType = 'Page' then
begin
Idx := Length(PageArr);
SetLength(PageArr, Idx + 1);
PageArr[Idx].PageObj := Child;
end
else if ChildType = 'Pages' then
begin
// intermediate node: recurse into its Kids
BuildPageIndexFromTree(Child.GetArray('/Kids'), PageArr);
end;
end;
end;
Αφού εκτελεστεί αυτό, το PageArr[0] είναι η πρώτη σελίδα που θα εμφάνιζε ένα πρόγραμμα προβολής, ανεξάρτητα από το πού βρίσκεται αυτό το αντικείμενο στη ροή byte. Τα ευρετήρια που μεταβιβάζονται από καλούντες και υποθέτουν τη σειρά εμφάνισης τώρα αντιστοιχίζονται σωστά, και τα σφάλματα εύρους σταματούν
Οι σκληρά κωδικοποιημένες (hard-coded) λύσεις επιδεινώνουν το πρόβλημα
Σε βάσεις κώδικα όπου η βασική αιτία δεν εντοπίστηκε ποτέ, είναι σύνηθες να βρίσκουμε ευρετικές επιδιορθώσεις (heuristic patches): εναλλαγή της πρώτης και της τελευταίας σελίδας εάν ο συνολικός αριθμός ισούται με 3, περιστροφή του ευρετηρίου για έγγραφα από μια συγκεκριμένη γεννήτρια, εφαρμογή μιας μετατόπισης (offset) όταν ο πρώτος αριθμός αντικειμένου υπερβαίνει ένα όριο. Κάθε μία από αυτές τις διορθώσεις ταιριάζει ακριβώς στο σύνολο των δοκιμαστικών αρχείων που υπήρχαν κατά τη συγγραφή της. Προσθέστε μια διαφορετική πηγή PDF και μία από τις επιδιορθώσεις ενεργοποιείται σε λάθος στιγμή, παράγοντας ένα ευρετήριο που είναι πλέον διπλά λάθος: λάθος επειδή υπολογίστηκε από έναν πίνακα εκτός σειράς, και λάθος ξανά επειδή εφαρμόστηκε μια μη εφαρμόσιμη αντιστοίχιση (mapping) από πάνω. Ο ελεγκτής εύρους το συλλαμβάνει κάπου παρακάτω (downstream) και το ίχνος στοίβας (stack trace) δεν δείχνει πουθενά χρήσιμα
Η μόνη παραγωγική διαδρομή είναι να αφαιρέσετε κάθε ευρετική αντιστοίχιση και να αντικαταστήσετε την κατασκευή του πίνακα σελίδων με έναν σωστό περίπατο δέντρου (tree walk). Μόλις τα ευρετήρια γίνουν σωστά εκ κατασκευής, δεν χρειάζονται επιδιορθώσεις και ο ελεγκτής εύρους γίνεται πλεονέκτημα (asset) αντί για εμπόδιο
Εάν συντηρείτε μια βιβλιοθήκη που εμφανίζει αυτό το μοτίβο, ενεργοποιήστε τον έλεγχο εύρους σε μια έκδοση Release προσωρινά και τρέξτε την ενάντια σε ένα διαφορετικό σώμα εγγράφων PDF: έγγραφα που παράγονται από Word, από LaTeX, από υλικολογισμικό σαρωτή, από βοηθητικά προγράμματα διαχωρισμού (split) PDF-to-PDF. Τα αρχεία που προκαλούν εξαιρέσεις είναι εκείνα των οποίων η σειρά αντικειμένων σελίδας αποκλίνει από τη σειρά διέλευσης που προϋποθέτει ο κώδικάς σας. Κάθε ένα από αυτά είναι ένα σημείο δεδομένων, όχι ένα ξεχωριστό σφάλμα
Για νέο κώδικα που καλεί μια βιβλιοθήκη PDF Delphi, η πρακτική συμβουλή είναι να αντιμετωπίζετε τον αριθμό σελίδων της βιβλιοθήκης ως έγκυρο και να μην μεταβιβάζετε ποτέ ένα ευρετήριο που προέρχεται από αριθμητικές πράξεις σε εξωτερικά δεδομένα χωρίς πρώτα να επιβεβαιώσετε ότι εμπίπτει στο 0..PageCount - 1. Το στοιχείο HotPDF εκθέτει τον επιλυμένο αριθμό σελίδων μέσω του THotPDF.PageCount μετά το BeginDoc ή μετά τη φόρτωση ενός εγγράφου. αυτή η τιμή αντικατοπτρίζει πάντα τη διέλευση του δέντρου σελίδων και είναι ασφαλής η χρήση της ως το ανώτερο όριο για οποιαδήποτε αριθμητική ευρετηρίου