Deschideți un xls vechi, îl salvați din nou, și formula add-in care apela o bibliotecă de analiză înregistrată indică acum o referință goală în interiorul registrului de lucru. HotXLS urmărește această corupție tăcută până la o presupunere greșită: că o înregistrare BIFF SupBook este fie self, fie un fișier extern. [MS-XLS] definește șapte feluri, nu două
De ce pierde un registru salvat legăturile add-in?
Pentru că testul de clasificare era structural, nu bazat pe tip. Scurtătura tradițională citește o înregistrare SupBook ($01AE), verifică dacă poartă marcatorul self și, dacă nu, tratează orice șir care urmează ca URL de document. Fiecare înregistrare care nu este niciuna dintre cele două cade într-o ramură implicită, iar ramura implicită este aproape întotdeauna „acesta este registrul de lucru”. O legătură add-in de suport, o legătură same-sheet, un slot neutilizat și o înregistrare trunchiată ajung toate să poarte aceeași etichetă greșită. Nimic nu aruncă o excepție cât timp se întâmplă asta: înregistrarea s-a parsat, formula s-a recompilat, fișierul s-a salvat fără avertisment, iar defectul apare trei săptămâni mai târziu când cineva observă o coloană de zerouri acolo unde era o conversie valutară. [MS-XLS] §2.4.271 descrie o înregistrare care poate fi o auto-referință, o referință same-sheet, un container de funcții add-in, un registru extern cu o cale virtuală și o tabelă de nume de foi, o legătură de date DDE sau OLE sau un substitut neutilizat — plus o a șaptea stare care nu figurează în specificație, dar există pe discuri reale: înregistrarea care nu se parsează. Remedierea nu este o euristică mai bună; este refuzul de a avea vreo euristică
Cele șapte feluri pe care le poate purta o înregistrare SupBook
HotXLS declară taxonomia legăturilor de suport ca o enumerare închisă în lxExternSheet.pas, iar fiecare decizie din aval comută pe ea. Nouă valori de enumerare acoperă cele șapte categorii, pentru că cazul DDE și OLE are nevoie de o stare provizorie înainte de a putea fi rezolvat:
type
TXLSSupportingLinkKind = (
slkUnknown, // parsare eșuată sau au rămas octeți în plus
slkSelf, // acest registru de lucru
slkSameSheet, // marcator U+0000
slkAddIn, // container de funcții add-in
slkExternalWorkbook, // cale virtuală + tabelă de nume de foi
slkDde, // rezolvat din flagurile ExternName
slkOle, // rezolvat din flagurile ExternName
slkDdeOrOle, // unul dintre cele două, nu se știe încă care
slkUnused); // substitut cu un singur spațiu
TXLSFormulaReferenceClass = (
frcInternal,
frcExternalWorkbook,
frcExternalOther,
frcUnknownOrMalformed);
TXLSXtiInfo = record
XtiIndex : Integer; // bazat pe zero, așa cum e stocat în ExternSheet.rgXTI
ExternID : Integer; // bazat pe unu, convenția internă
SupBookIndex: Integer;
Sheet1Index : Integer;
Sheet2Index : Integer;
LinkKind : TXLSSupportingLinkKind;
end;
Dispecerizarea e condusă de sentinele, nu de șiruri. O valoare de câmp $0401 marchează înregistrarea self. Un număr de foi de unu asociat cu $3A01 marchează un container add-in. Doar o valoare în intervalul 1 până la $00FF înseamnă că urmează o cale virtuală codificată, și abia atunci HotXLS decodează un șir. Orice în afara acestor trei forme rămâne slkUnknown, iar o înregistrare a cărei tabelă de nume de foi nu consumă exact corpul înregistrării este retrogradată înapoi la slkUnknown chiar dacă antetul părea plauzibil
De ce marcatorul same-sheet se decodează ca șir gol?
Pentru că cititorul generic de șiruri BIFF distruge octetul de care depinde clasificarea. Legătura de suport same-sheet este un șir de un singur caracter, iar acel caracter e U+0000, iar TXLSBlob.GetBiffString îl întoarce ca WideString gol, de nedistins de o cale efectiv goală — exact intrarea la care o euristică de auto-referință răspunde „self”. HotXLS citește de aceea primul punct de cod brut din corpul înregistrării în loc să aibă încredere în valoarea decodată:
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) // comprimat, un octet
else
FirstChar := Data.GetWord(StringOffset + 3); // lat, doi octeți
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;
Observați ramura comprimat-versus-lat. Octetul de opțiuni stă la un decalaj fix față de antetul șirului, iar primul punct de cod are un octet sau doi în funcție de bitul 0, deci citirea lui necondiționat ca octet funcționează pe majoritatea fișierelor și eșuează pe cele scrise de versiuni localizate — cea mai proastă distribuție posibilă pentru un bug. Substitutul neutilizat e prins la fel, prin încărcătura lui literală de un singur spațiu, iar cazul DDE sau OLE prin separatorul U+0003 încorporat în calea codificată
De ce nu pot fi separate DDE și OLE la momentul SupBook?
Pentru că înregistrarea SupBook nu poartă biții care diferențiază. Vă spune că legătura este una dintre cele două; flagurile fOle și fOleLink care decid care stau în înregistrarea ExternName ($0023) ce sosește mai târziu în flux. HotXLS notează slkDdeOrOle la momentul parsării și îl restrânge în ParseExternalName, iar dacă nu sosește niciodată vreun ExternName, felul rămâne provizoriu pentru totdeauna — ceea ce e corect, pentru că fișierul chiar nu spune. Fiecare consumator din aval tratează valoarea provizorie ca o valoare reală, nu ca una lipsă, astfel încât niciun apelant nu trebuie să inventeze o departajare. A ghici „probabil DDE” aici ar cumpăra o enumerare mai îngrijită și o clasă de răspunsuri greșite pe care nimeni nu le-ar putea urmări:
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;
Indicii XTI sunt bazați pe zero pe disc și pe unu în interior
HotXLS face conversia de tip off-by-one exact o dată, în momentul în care un token intră în arborele sintactic intern, și nicăieri altundeva. PtgNameX.ixti ([MS-XLS] §2.5.198.85) este un index cu bază zero în tabloul rgXTI al înregistrării ExternSheet ($0017, §2.4.106), în timp ce convenția internă ExternID a bibliotecii are baza unu, cu zero rezervat pentru „fără foaie externă”. Calea de citire BIFF8 face FExternID := wValue + 1 când decodează un token tNameX, iar calea de scriere emite StoreExternID - 1, lăsând neatinse vederea brută a tokenurilor și semanticile de pe disc. A greși aici e neobișnuit de greu de prins: numele definite externe se rezolvă la intrarea vecină, iar într-un fișier cu o singură intrare XTI indexul 0 devine indexul 1, ratează și numele se degradează tăcut. O regresie care exersează doar textul de formulă recompilat nu o vede niciodată, pentru că recompilarea nu atinge deloc indexul de pe disc — aceeași capcană care face ca numele definite care traversează foi și registre să merite testate contra fluxurilor reale de octeți. Rezolvarea e limitată la ambele capete: TlxExternSheetSheet.TryResolveXti întoarce False pentru un index negativ sau o intrare lipsă, TXLSSupBook.TryGetKind întoarce False pentru un index SupBook în afara tabloului, iar ClassifyXti mapează apoi slkSelf și slkSameSheet la frcInternal, slkExternalWorkbook la frcExternalWorkbook, iar slkAddIn, slkDde, slkOle și slkDdeOrOle la frcExternalOther. Tot restul, inclusiv fiecare cale în afara intervalului, aterizează pe frcUnknownOrMalformed
Clasificarea unei formule înainte de a o îngheța
TXLSCompiledFormula.ClassifyReferences scanează direct fluxul de tokenuri BIFF păstrat în loc să decompileze formula și să caute paranteze drepte. Vânarea parantezelor în textul formulei este o euristică textuală îmbrăcată în haina unui parser: se potrivește pe literale de șir, se potrivește pe referințe structurate și ratează complet numele definite externe, de vreme ce acestea nu poartă paranteze în forma decompilată. Scanarea de tokenuri privește doar PtgNameX, PtgRef3d, PtgArea3d, PtgRefErr3d și PtgAreaErr3d, revenind la o parcurgere a arborelui sintactic când niciun flux BIFF nu supraviețuiește. Îmbinarea e deliberat pesimistă — prioritatea fixă este frcUnknownOrMalformed, apoi frcExternalWorkbook, apoi frcExternalOther, apoi frcInternal — astfel încât un singur token ilizibil otrăvește întreaga formulă. Pentru un nume definit extern se validează și indexul numelui: cu bază unu, în interval și susținut de o înregistrare ExternName reținută
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 are baza unu
begin
Sheet := Wb.Sheets[i];
// îngheață DOAR formulele clasificate frcExternalWorkbook;
// referințele interne, add-in, DDE/OLE și malformate rămân formule
Sheet.ConvertFormulasToValues(True);
end;
Wb.SaveAs('quarterly-detached.xls');
finally
Wb.Free;
end;
end;
Parametrul OnlyExternal este locul unde taxonomia își plătește prețul. Înghețarea unei formule e ireversibilă, deci operația trebuie să dovedească că o referință este un registru extern, nu doar să bănuiască. Apelurile add-in supraviețuiesc, legăturile DDE și OLE supraviețuiesc, iar orice nu a putut fi înțeles complet de parser supraviețuiește, pentru că rezultatul sigur al incertitudinii este să nu schimbați nimic. Aceeași disciplină guvernează relegarea formulelor copiate între registre, unde o referință clasificată greșit se relege la registrul greșit în loc să eșueze zgomotos
Înregistrările care nu se parsează sunt rescrise neatinse
HotXLS păstrează încărcătura SupBook originală și o reemite octet cu octet când înregistrarea n-a fost niciodată editată. Un eșec de parsare stabilește slkUnknown și golește starea derivată, dar corpul capturat rămâne în FRawData, iar calea de salvare îl preferă oricărei reconstrucții atâta timp cât elementul nu e murdar și nu e înregistrarea self. Alternativa — normalizarea unei înregistrări neparsate într-o auto-referință, ca scriitorul să aibă ceva bine format de emis — transformă o înregistrare pe care nu ați înțeles-o într-una definitiv greșită. Acest principiu este același contract aplicat proiectelor VBA și referințelor lor externe pe parcursul unui ciclu încărcare-salvare, și e diferența dintre o bibliotecă care face round-trip pe fișiere reale și una care face round-trip pe fișierele pe care testele sale se întâmplă să le conțină. Un registru care a trecut prin cincisprezece ani de versiuni Excel, un generator de rapoarte și două unelte de migrare va conține înregistrări pe care niciun om în viață nu le-a proiectat. Rescrieți-le așa cum le-ați găsit
Clasificarea tipizată a înregistrărilor SupBook și XTI a fost livrată în HotXLS 2.361.2 până la 2.361.4, împreună cu rezolvarea XTI limitată și calea mai sigură ConvertFormulasToValues descrisă aici. Dacă întrețineți cod Delphi sau C++Builder care citește fișiere xls vechi cu apeluri add-in, legături DDE sau OLE sau nume definite externe, componenta Delphi de foi de calcul HotXLS gestionează întreaga taxonomie nativ, fără instalare Excel și fără automatizare OLE pe mașina care face lucrarea