Articol tehnic

Clasificarea linkurilor BIFF SupBook și XTI în Delphi

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

Scara condusă de sentinele pe care HotXLS o folosește pentru a clasifica o înregistrare BIFF SupBook în șapte feluri, decodând un șir doar pentru valorile din intervalul de căi codificate și revenind la un fel necunoscut în locul unei ramuri implicite
Fiecare fel e atins printr-o sentinelă, nu printr-un test de șir, iar o înregistrare care nu se potrivește cu nicio formă rămâne necunoscută în loc să cadă într-o ramură implicită care înseamnă acest registru de lucru

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 HotXLS citește primul punct de cod brut din corpul unei înregistrări BIFF SupBook în locul șirului decodat, pentru că cititorul generic de șiruri transformă marcatorul same-sheet U+0000 într-o valoare goală
Marcatorul same-sheet este un șir de un caracter, iar caracterul e U+0000, astfel încât cititorul generic de șiruri îl comprimă într-o valoare goală și doar punctul de cod brut de la decalajul octetului de opțiuni îl păstrează

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

HotXLS convertind indexul XTI cu bază zero al unui token BIFF PtgNameX în ExternID intern cu bază unu într-un singur punct, cu rezolvare limitată la ambele capete și harta de clasificare care o consumă
Decalajul de unu între indexul de pe disc cu bază zero și ExternID intern cu bază unu se aplică o dată, când tokenul intră în arborele sintactic, iar fiecare index nerezolvabil aterizează pe clasa malformat

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