Άνοιξε ένα παλιό xls, αποθήκευσέ το ξανά, και ο τύπος add-in που καλούσε μια καταχωρημένη βιβλιοθήκη ανάλυσης δείχνει τώρα σε μια κενή αναφορά μέσα στο ίδιο το βιβλίο εργασίας. Το HotXLS ανιχνεύει αυτή τη σιωπηλή φθορά σε μια κακή υπόθεση: ότι μια εγγραφή BIFF SupBook είναι είτε self είτε εξωτερικό αρχείο. Το [MS-XLS] ορίζει επτά είδη, όχι δύο
Γιατί ένα αποθηκευμένο βιβλίο εργασίας χάνει τις συνδέσεις add-in του;
Επειδή το τεστ ταξινόμησης ήταν δομικό αντί τυπολογικό. Η παραδοσιακή συντόμευση διαβάζει μια εγγραφή SupBook ($01AE), ελέγχει αν κουβαλά τον δείκτη self, και αν όχι, μεταχειρίζεται όποια συμβολοσειρά ακολουθεί ως URL εγγράφου. Κάθε εγγραφή που δεν είναι ούτε το ένα ούτε το άλλο πέφτει σε έναν προεπιλεγμένο κλάδο, και ο προεπιλεγμένος κλάδος είναι σχεδόν πάντα «αυτό είναι το ίδιο το βιβλίο εργασίας». Μια υποστηρικτική σύνδεση add-in, μια σύνδεση ίδιου φύλλου, μια αχρησιμοποίητη θέση και μια περικομμένη εγγραφή καταλήγουν όλες φορώντας την ίδια λάθος ετικέτα. Τίποτα δεν πετάει εξαίρεση όσο αυτό συμβαίνει: η εγγραφή αναλύθηκε, ο τύπος ξαναμεταγλωττίστηκε, το αρχείο αποθηκεύτηκε χωρίς προειδοποίηση, και το ελάττωμα αναδύεται τρεις εβδομάδες μετά όταν κάποιος προσέχει μια στήλη μηδενικών εκεί που υπήρχε μετατροπή νομίσματος. Το [MS-XLS] §2.4.271 περιγράφει μια εγγραφή που μπορεί να είναι self-αναφορά, αναφορά ίδιου φύλλου, δοχείο συναρτήσεων add-in, εξωτερικό βιβλίο εργασίας με εικονική διαδρομή και πίνακα ονομάτων φύλλων, σύνδεση δεδομένων DDE ή OLE, ή αχρησιμοποίητο placeholder — και μια έβδομη κατάσταση που δεν είναι στην προδιαγραφή αλλά υπάρχει σε πραγματικούς δίσκους, η εγγραφή που δεν αναλύεται. Η λύση δεν είναι μια καλύτερη ευρετική· είναι η άρνηση ύπαρξης ευρετικής καθόλου
Τα επτά είδη που μπορεί να κουβαλά μια εγγραφή SupBook
Το HotXLS δηλώνει την ταξινομία supporting-link ως κλειστή απαρίθμηση στο lxExternSheet.pas, και κάθε κατάντη απόφαση κάνει switch πάνω της. Εννέα τιμές απαρίθμησης καλύπτουν τις επτά κατηγορίες, επειδή η περίπτωση DDE και OLE χρειάζεται μια προσωρινή κατάσταση πριν επιλυθεί:
type
TXLSSupportingLinkKind = (
slkUnknown, // απέτυχε η ανάλυση, ή έμειναν byte στο τέλος
slkSelf, // αυτό το βιβλίο εργασίας
slkSameSheet, // δείκτης U+0000
slkAddIn, // δοχείο συναρτήσεων add-in
slkExternalWorkbook, // εικονική διαδρομή + πίνακας ονομάτων φύλλων
slkDde, // επιλύεται από τα flags του ExternName
slkOle, // επιλύεται από τα flags του ExternName
slkDdeOrOle, // ένα από τα δύο, ακόμα άγνωστο ποιο
slkUnused); // placeholder ενός μοναδικού κενού
TXLSFormulaReferenceClass = (
frcInternal,
frcExternalWorkbook,
frcExternalOther,
frcUnknownOrMalformed);
TXLSXtiInfo = record
XtiIndex : Integer; // zero-based, όπως αποθηκεύεται στο ExternSheet.rgXTI
ExternID : Integer; // one-based, η εσωτερική σύμβαση
SupBookIndex: Integer;
Sheet1Index : Integer;
Sheet2Index : Integer;
LinkKind : TXLSSupportingLinkKind;
end;
Η αποστολή βασίζεται σε sentinels, όχι σε συμβολοσειρές. Τιμή πεδίου $0401 σηματοδοτεί την εγγραφή self. Πλήθος φύλλων ένα μαζί με $3A01 σηματοδοτεί δοχείο add-in. Μόνο τιμή στο εύρος 1 έως $00FF σημαίνει ότι ακολουθεί κωδικοποιημένη εικονική διαδρομή, και μόνο τότε το HotXLS αποκωδικοποιεί καθόλου συμβολοσειρά. Οτιδήποτε εκτός αυτών των τριών σχημάτων μένει slkUnknown, και εγγραφή της οποίας ο πίνακας ονομάτων φύλλων δεν καταναλώνει ακριβώς το σώμα της εγγραφής υποβαθμίζεται πίσω σε slkUnknown ακόμα κι όταν η κεφαλή έμοιαζε πιθανή
Γιατί ο δείκτης same-sheet αποκωδικοποιείται ως κενή συμβολοσειρά;
Επειδή ο γενικής χρήσης αναγνώστης συμβολοσειρών BIFF καταστρέφει το byte από το οποίο εξαρτάται η ταξινόμηση. Η υποστηρικτική σύνδεση same-sheet είναι συμβολοσειρά ενός χαρακτήρα του οποίου ο μοναδικός χαρακτήρας είναι U+0000, και το TXLSBlob.GetBiffString την επιστρέφει ως κενό WideString, αδιακρίτως από πραγματικά κενή διαδρομή — που είναι ακριβώς η είσοδος στην οποία μια ευρετική self-αναφορά απαντά «self». Το HotXLS λοιπόν διαβάζει τον ακατέργαστο πρώτο code point από το σώμα της εγγραφής αντί να εμπιστευτεί την αποκωδικοποιημένη τιμή:
StringOffset := offset;
FDocUrl := Data.GetBiffString(offset, False, True);
FirstChar := $FFFF;
if val = 1 then
begin
StringOptions := Data.GetByte(StringOffset + 2);
if (StringOptions and $01) = 0 then
FirstChar := Data.GetByte(StringOffset + 3) // συμπιεσμένο, ένα byte
else
FirstChar := Data.GetWord(StringOffset + 3); // φαρδύ, δύο bytes
end;
if FirstChar = 0 then
FKind := slkSameSheet
else if (Length(FDocUrl) = 1) and (FDocUrl[1] = WideChar(#32)) then
FKind := slkUnused
else if Pos(WideChar(#3), FDocUrl) > 0 then
FKind := slkDdeOrOle
else if FDocUrl <> '' then
FKind := slkExternalWorkbook;
Πρόσεξε τον κλάδο συμπιεσμένου-έναντι-φαρδύ. Το byte επιλογών κάθεται σε σταθερή μετατόπιση από τη κεφαλή της συμβολοσειράς και ο πρώτος code point είναι ένα ή δύο bytes ανάλογα με το bit 0, οπότε η άνευ όρων ανάγνωσή του ως byte δουλεύει στα περισσότερα αρχεία και αποτυγχάνει σε αυτά που έγραψαν localized εκδόσεις — η χειρότερη δυνατή κατανομή για bug. Το placeholder αχρησιμοποίητης θέσης πιάνεται με τον ίδιο τρόπο, από το κυριολεκτικό payload ενός κενού, και η περίπτωση DDE ή OLE από τον διαχωριστή U+0003 ενσωματωμένο στην κωδικοποιημένη διαδρομή
Γιατί DDE και OLE δεν χωρίζονται τη στιγμή του SupBook;
Επειδή η εγγραφή SupBook δεν κουβαλά τα διακριτικά bits. Σου λέει ότι η σύνδεση είναι ένα από τα δύο· τα flags fOle και fOleLink που αποφασίζουν ποιο μένουν στην εγγραφή ExternName ($0023) που φτάνει αργότερα στη ροή. Το HotXLS καταγράφει slkDdeOrOle τη στιγμή της ανάλυσης και το στενεύει στο ParseExternalName, και αν δεν έρθει ποτέ ExternName το είδος μένει προσωρινό για πάντα — που είναι σωστό, γιατί το αρχείο πραγματικά δεν το λέει. Κάθε καταναλωτής κατάντη μεταχειρίζεται αυτή την προσωρινή τιμή ως πραγματική τιμή και όχι ως ελλιπή, οπότε κανένας καλών δεν χρειάζεται να επινοήσει tiebreak. Το να μαντέψεις «πιθανόν DDE» εδώ θα αγόραζε μια πιο τακτοποιημένη απαρίθμηση και μια κατηγορία λάθος απαντήσεων που κανείς δεν θα μπορούσε να ιχνηλατήσει πίσω:
if FKind = slkDdeOrOle then
begin
if Data.DataLength < 2 then
Exit;
Flags := Data.GetWord(0);
if (Flags and $0010) <> 0 then
FKind := slkOle
else if (Flags and $0008) <> 0 then
FKind := slkDde;
end;
Τα ευρετήρια XTI είναι zero-based στον δίσκο και one-based στο εσωτερικό
Το HotXLS εκτελεί τη μετατροπή off-by-one ακριβώς μία φορά, στο σημείο που ένα token μπαίνει στο εσωτερικό δέντρο σύνταξης, και πουθενά αλλού. Το PtgNameX.ixti ([MS-XLS] §2.5.198.85) είναι ευρετήριο zero-based στον πίνακα rgXTI της εγγραφής ExternSheet ($0017, §2.4.106), ενώ η εσωτερική σύμβαση ExternID της βιβλιοθήκης είναι one-based με το μηδέν δεσμευμένο για «κανένα εξωτερικό φύλλο». Η διαδρομή ανάγνωσης BIFF8 κάνει FExternID := wValue + 1 όταν αποκωδικοποιεί token tNameX και η διαδρομή εγγραφής εκπέμπει StoreExternID - 1, αφήνοντας την ακατέργαστη όψη token και τη σημασιολογία στον δίσκο ανέγγιχτες. Να το κάνεις λάθος είναι ασυνήθιστα δύσκολο να πιαστεί: εξωτερικά καθορισμένα ονόματα επιλύονται στη γειτονική καταχώρηση, και σε αρχείο με μία καταχώρηση XTI το ευρετήριο 0 γίνεται 1, χάνει, και το όνομα υποβαθμίζεται σιωπηλά. Ένα regression που εξασκεί μόνο επαναμεταγλωττισμένο κείμενο τύπου δεν το βλέπει ποτέ, επειδή η επαναμεταγλώττιση δεν αγγίζει καθόλου το ευρετήριο του δίσκου — η ίδια παγίδα που κάνει τα καθορισμένα ονόματα που εκτείνονται σε φύλλα και βιβλία εργασίας να αξίζουν τεστ σε πραγματικά streams bytes. Η επίλυση είναι φραγμένη στα δύο άκρα: το TlxExternSheetSheet.TryResolveXti επιστρέφει False για αρνητικό ευρετήριο ή ελλιπή καταχώρηση, το TXLSSupBook.TryGetKind επιστρέφει False για ευρετήριο SupBook εκτός πίνακα, και το ClassifyXti μετά αντιστοιχίζει slkSelf και slkSameSheet σε frcInternal, slkExternalWorkbook σε frcExternalWorkbook, και slkAddIn, slkDde, slkOle και slkDdeOrOle σε frcExternalOther. Ό,τι άλλο, κάθε διαδρομή εκτός εύρους συμπεριλαμβανομένης, προσγειώνεται στο frcUnknownOrMalformed
Ταξινομώντας έναν τύπο πριν τον παγώσεις
Το TXLSCompiledFormula.ClassifyReferences σαρώνει απευθείας το διατηρημένο stream tokens BIFF αντί να απομεταφράσει τον τύπο και να ψάξει για αγκύλες. Το κυνήγι αγκυλών σε κείμενο τύπου είναι ευρετική κειμένου ντυμένη παλτό parser: ταιριάζει literals συμβολοσειρών, ταιριάζει δομημένες αναφορές, και χάνει τελείως τα εξωτερικά καθορισμένα ονόματα, αφού αυτά δεν κουβαλούν αγκύλες σε απομεταφρασμένη μορφή. Η σάρωση tokens κοιτάζει μόνο PtgNameX, PtgRef3d, PtgArea3d, PtgRefErr3d και PtgAreaErr3d, με επιστροφή σε διάσχιση δέντρου σύνταξης όταν δεν επιβιώνει stream BIFF. Η συγχώνευση είναι σκόπιμα απαισιόδοξη — η σταθερή προτεραιότητα είναι frcUnknownOrMalformed, μετά frcExternalWorkbook, μετά frcExternalOther, μετά frcInternal — οπότε ένα μοναδικό δυσανάγνωστο token δηλητηριάζει όλο τον τύπο. Για εξωτερικό καθορισμένο όνομα το ευρετήριο ονόματος επικυρώνεται κι αυτό: one-based, στο εύρος, και υποστηριζόμενο από διατηρημένη εγγραφή ExternName
var
Wb : TXLSWorkbook;
Sheet: TXLSWorksheet;
i : Integer;
begin
Wb := TXLSWorkbook.Create;
try
Wb.Open('quarterly.xls');
for i := 1 to Wb.Sheets.Count do // Το Sheets βασίζεται στο ένα
begin
Sheet := Wb.Sheets[i];
// Παγώνει ΜΟΝΟ τύπους ταξινομημένους frcExternalWorkbook;
// εσωτερικές, add-in, DDE/OLE και κακοσχηματισμένες αναφορές μένουν τύποι
Sheet.ConvertFormulasToValues(True);
end;
Wb.SaveAs('quarterly-detached.xls');
finally
Wb.Free;
end;
end;
Η παράμετρος OnlyExternal είναι εκεί που η ταξινομία πληρώνει το κόστος της. Το πάγωμα ενός τύπου είναι μη αναστρέψιμο, άρα η λειτουργία πρέπει να αποδείξει ότι μια αναφορά είναι εξωτερικό βιβλίο εργασίας και όχι απλώς να την υποψιαστεί. Οι κλήσεις add-in επιβιώνουν, οι συνδέσεις DDE και OLE επιβιώνουν, και ό,τι δεν μπόρεσε ο parser να καταλάβει πλήρως επιβιώνει, επειδή το ασφαλές αποτέλεσμα της αβεβαιότητας είναι να μην αλλάξει τίποτα. Η ίδια πειθαρχία κυβερνά το rebinding τύπων που αντιγράφονται μεταξύ βιβλίων εργασίας, όπου μια κακοταξινομημένη αναφορά επαναδένεται σε λάθος βιβλίο αντί να αποτύχει δυνατά
Οι εγγραφές που δεν αναλύονται ξαναγράφονται ανέγγιχτες
Το HotXLS κρατά το αρχικό payload SupBook και το επανεκπέμπει byte-προς-byte όταν η εγγραφή δεν επεξεργάστηκε ποτέ. Αποτυχία ανάλυσης θέτει slkUnknown και καθαρίζει την παράγωγη κατάσταση, αλλά το καταγεγραμμένο σώμα μένει στο FRawData και η διαδρομή αποθήκευσης το προτιμά από κάθε ανακατασκευή όσο το στοιχείο δεν είναι dirty και δεν είναι η εγγραφή self. Η εναλλακτική — η κανονικοποίηση μιας μη αναλυμένης εγγραφής σε self-αναφορά ώστε ο writer να έχει κάτι καλά σχηματισμένο να εκπέμψει — μετατρέπει μια εγγραφή που δεν κατάλαβες σε εγγραφή που είναι οριστικά λάθος. Η αρχή είναι το ίδιο συμβόλαιο που εφαρμόζεται στα VBA projects και τις εξωτερικές αναφορές τους σε έναν κύκλο load-and-save, και είναι η διαφορά μεταξύ βιβλιοθήκης που κάνει round-trip πραγματικά αρχεία και βιβλιοθήκης που κάνει round-trip τα αρχεία που τυχαίνει να περιέχει η σουίτα τεστ της. Ένα βιβλίο εργασίας που πέρασε από δεκαπέντε χρόνια εκδόσεων Excel, μια γεννήτρια αναφορών και δύο εργαλεία μετάπτωσης θα περιέχει εγγραφές που δεν σχεδίασε κανένας εν ζωή. Γράψέ τες πίσω όπως τις βρήκες
Η ταξινόμηση των εγγραφών SupBook και XTI βάσει τύπου κυκλοφόρησε στο HotXLS 2.361.2 έως 2.361.4, μαζί με φραγμένη επίλυση XTI και τη πιο ασφαλή διαδρομή ConvertFormulasToValues που περιγράφεται εδώ. Αν συντηρείς κώδικα Delphi ή C++Builder που διαβάζει legacy xls αρχεία με κλήσεις add-in, συνδέσεις DDE ή OLE, ή εξωτερικά καθορισμένα ονόματα, το HotXLS Delphi spreadsheet component χειρίζεται όλη την ταξινομία εγγενώς, χωρίς εγκατάσταση Excel και χωρίς OLE automation στο μηχάνημα που δουλεύει