Articolo tecnico

Classificazione link esterni BIFF SupBook e XTI in Delphi

Aprite un vecchio xls, salvatelo di nuovo, e la formula add-in che chiamava una libreria di analisi registrata ora punta a un riferimento vuoto dentro la cartella di lavoro stessa. HotXLS riconduce quella corruzione silenziosa a un presupposto sbagliato: che un record BIFF SupBook sia o self o un file esterno. [MS-XLS] definisce sette tipi, non due

Perché una cartella di lavoro salvata perde i propri link add-in?

Perché il test di classificazione era strutturale invece che tipizzato. La scorciatoia tradizionale legge un record SupBook ($01AE), controlla se porta il marcatore self e, in caso negativo, tratta la stringa che segue come un URL di documento. Ogni record che non è nessuna di queste due cose cade in un ramo predefinito, e il ramo predefinito è quasi sempre "questa è la cartella di lavoro stessa". Un link di supporto add-in, un link stesso-foglio, uno slot inutilizzato e un record troncato finiscono tutti per indossare la stessa etichetta sbagliata. Nulla lancia eccezioni mentre accade: il record viene analizzato, la formula ricompilata, il file salvato senza avvisi, e il difetto emerge tre settimane dopo quando qualcuno nota una colonna di zeri dove una volta c'era una conversione di valuta. [MS-XLS] §2.4.271 descrive un record che può essere un auto-riferimento, un riferimento stesso-foglio, un contenitore di funzioni add-in, una cartella di lavoro esterna con percorso virtuale e tabella dei nomi dei fogli, un link dati DDE o OLE, o un segnaposto inutilizzato — e un settimo stato che non è nella specifica ma esiste sui dischi reali, il record che non si analizza. La correzione non è un'euristica migliore; è rifiutarsi di avere un'euristica

I sette tipi che un record SupBook può contenere

HotXLS dichiara la tassonomia dei link di supporto come un'enumerazione chiusa in lxExternSheet.pas, e ogni decisione a valle commuta su di essa. Nove valori di enumerazione coprono le sette categorie, perché il caso DDE e OLE richiede uno stato provvisorio prima di poter essere risolto:

type
  TXLSSupportingLinkKind = (
    slkUnknown,           // parse non riuscito, o byte finali residui
    slkSelf,              // questa cartella di lavoro
    slkSameSheet,         // marcatore U+0000
    slkAddIn,             // contenitore di funzioni add-in
    slkExternalWorkbook,  // percorso virtuale + tabella dei nomi dei fogli
    slkDde,               // risolto dai flag ExternName
    slkOle,               // risolto dai flag ExternName
    slkDdeOrOle,          // uno dei due, non ancora noto quale
    slkUnused);           // segnaposto di un singolo spazio

  TXLSFormulaReferenceClass = (
    frcInternal,
    frcExternalWorkbook,
    frcExternalOther,
    frcUnknownOrMalformed);

  TXLSXtiInfo = record
    XtiIndex    : Integer;   // a base zero, come memorizzato in ExternSheet.rgXTI
    ExternID    : Integer;   // a base uno, la convenzione interna
    SupBookIndex: Integer;
    Sheet1Index : Integer;
    Sheet2Index : Integer;
    LinkKind    : TXLSSupportingLinkKind;
  end;

Il dispatch è guidato da sentinella, non da stringhe. Un valore di campo $0401 contrassegna il record self. Un conteggio fogli di uno abbinato a $3A01 contrassegna un contenitore add-in. Solo un valore nell'intervallo da 1 a $00FF significa che segue un percorso virtuale codificato, e solo allora HotXLS decodifica una stringa. Tutto ciò che è fuori da quelle tre forme resta slkUnknown, e un record la cui tabella dei nomi dei fogli non consuma esattamente il corpo del record viene declassato di nuovo a slkUnknown anche quando la testa sembrava plausibile

La scala guidata da sentinella che HotXLS usa per classificare un record BIFF SupBook in sette tipi, decodificando una stringa solo per i valori nell'intervallo di percorso codificato e ricadendo su un tipo sconosciuto anziché su un ramo predefinito che significa questa cartella di lavoro
Ogni tipo viene raggiunto da una sentinella anziché da un test su stringa, e un record che non corrisponde a nessuna delle forme resta sconosciuto invece di cadere in un ramo predefinito che significa questa cartella di lavoro

Perché il marcatore stesso-foglio si decodifica come stringa vuota?

Perché il lettore di stringhe BIFF generico distrugge il byte da cui dipende la classificazione. Il link di supporto stesso-foglio è una stringa di un carattere il cui unico carattere è U+0000, e TXLSBlob.GetBiffString la restituisce come una WideString vuota, indistinguibile da un percorso genuinamente vuoto — che è esattamente l'input a cui un'euristica di auto-riferimento risponde "self". HotXLS quindi legge il primo code point grezzo dal corpo del record invece di fidarsi del valore decodificato:

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)     // compresso, un byte
  else
    FirstChar := Data.GetWord(StringOffset + 3);    // wide, due byte
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;

Notate il ramo compresso-contro-wide. Il byte di opzioni siede a un offset fisso dall'header della stringa e il primo code point è di un byte o due a seconda del bit 0, quindi leggerlo come byte incondizionatamente funziona sulla maggior parte dei file e fallisce su quelli scritti da build localizzate — la distribuzione peggiore possibile per un bug. Il segnaposto inutilizzato viene beccato allo stesso modo, dal suo payload letterale di un singolo spazio, e il caso DDE o OLE dal separatore U+0003 incorporato nel percorso codificato

Perché HotXLS legge il primo code point grezzo dal corpo di un record BIFF SupBook anziché la stringa decodificata, dato che il lettore di stringhe generico trasforma il marcatore stesso-foglio U+0000 in un valore vuoto
Il marcatore stesso-foglio è una stringa di un carattere il cui carattere è U+0000, quindi il lettore di stringhe generico lo ripiega in un valore vuoto e solo il code point grezzo all'offset del byte di opzioni lo preserva

Perché DDE e OLE non si possono separare al momento del SupBook?

Perché il record SupBook non porta i bit distintivi. Dice che il link è uno dei due; i flag fOle e fOleLink che decidono quale risiedono nel record ExternName ($0023) che arriva più avanti nello stream. HotXLS registra slkDdeOrOle al momento del parse e lo restringe in ParseExternalName, e se nessun ExternName arriva mai il tipo resta provvisorio per sempre — il che è corretto, perché il file genuinamente non lo dice. Ogni consumatore a valle tratta quel valore provvisorio come un valore reale anziché mancante, così nessun chiamante deve inventarsi uno spareggio. Indovinare "probabilmente DDE" qui comprerebbe un'enumerazione più ordinata e una classe di risposte sbagliate che nessuno saprebbe ricondurre:

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;

Gli indici XTI sono a base zero su disco e a base uno dentro

HotXLS esegue la conversione off-by-one esattamente una volta, nel punto in cui un token entra nell'albero sintattico interno, e da nessun'altra parte. PtgNameX.ixti ([MS-XLS] §2.5.198.85) è un indice a base zero nell'array rgXTI del record ExternSheet ($0017, §2.4.106), mentre la convenzione interna ExternID della libreria è a base uno con zero riservato a "nessun foglio esterno". Il percorso di lettura BIFF8 esegue FExternID := wValue + 1 quando decodifica un token tNameX e il percorso di scrittura emette StoreExternID - 1, lasciando intatti la vista grezza dei token e la semantica su disco. Sbagliare questo è insolitamente difficile da beccare: i nomi definiti esterni si risolvono sulla voce vicina, e in un file con una sola voce XTI l'indice 0 diventa indice 1, manca, e il nome degrada silenziosamente. Una regression che esercita solo testo di formule ricompilate non lo vede mai, perché la ricompilazione non tocca mai l'indice su disco — la stessa trappola che rende i nomi definiti che attraversano fogli e cartelle di lavoro degni di test contro stream di byte reali. La risoluzione è limitata da entrambe le estremità: TlxExternSheetSheet.TryResolveXti restituisce False per un indice negativo o una voce mancante, TXLSSupBook.TryGetKind restituisce False per un indice SupBook fuori dall'array, e ClassifyXti quindi mappa slkSelf e slkSameSheet a frcInternal, slkExternalWorkbook a frcExternalWorkbook, e slkAddIn, slkDde, slkOle e slkDdeOrOle a frcExternalOther. Tutto il resto, ogni percorso fuori intervallo incluso, atterra su frcUnknownOrMalformed

HotXLS che converte l'indice XTI a base zero di un token BIFF PtgNameX nel suo ExternID interno a base uno in un unico punto, con risoluzione limitata su entrambe le estremità e la mappa di classificazione che lo consuma
L'off-by-one tra l'indice su disco a base zero e l'ExternID interno a base uno viene applicato una volta, quando un token entra nell'albero sintattico, e ogni indice irrisolvibile atterra sulla classe malformata

Classificare una formula prima di congelarla

TXLSCompiledFormula.ClassifyReferences scansiona direttamente lo stream di token BIFF preservato invece di decompilare la formula e cercare parentesi quadre. La caccia alle parentesi nel testo delle formule è un'euristica testuale con il cappotto di un parser: matcha stringhe letterali, matcha riferimenti strutturati e ignora completamente i nomi definiti esterni, visto che nella forma decompilata non portano parentesi. La scansione dei token guarda solo a PtgNameX, PtgRef3d, PtgArea3d, PtgRefErr3d e PtgAreaErr3d, ricadendo su una passeggiata dell'albero sintattico quando nessuno stream BIFF sopravvive. La fusione è deliberatamente pessimistica — la priorità fissa è frcUnknownOrMalformed, poi frcExternalWorkbook, poi frcExternalOther, poi frcInternal — così un singolo token illeggibile avvelena l'intera formula. Per un nome definito esterno l'indice del nome viene validato anch'esso: a base uno, nell'intervallo e supportato da un record ExternName trattenuto

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 è a base uno
    begin
      Sheet := Wb.Sheets[i];
      // congela SOLO le formule classificate frcExternalWorkbook;
      // i riferimenti interni, add-in, DDE/OLE e malformati restano formule
      Sheet.ConvertFormulasToValues(True);
    end;
    Wb.SaveAs('quarterly-detached.xls');
  finally
    Wb.Free;
  end;
end;

Il parametro OnlyExternal è il punto in cui la tassonomia ripaga se stessa. Congelare una formula è irreversibile, quindi l'operazione deve dimostrare che un riferimento è una cartella di lavoro esterna anziché limitarsi a sospettarlo. Le chiamate add-in sopravvivono, i link DDE e OLE sopravvivono, e tutto ciò che il parser non ha potuto capire pienamente sopravvive, perché l'esito sicuro dell'incertezza è non cambiare nulla. La stessa disciplina governa la rilegatura delle formule copiate tra cartelle di lavoro, dove un riferimento classificato male si rilega al libro sbagliato invece di fallire rumorosamente

I record che non si analizzano vengono riscritti intatti

HotXLS conserva il payload SupBook originale e lo riemette byte per byte quando il record non è mai stato modificato. Un fallimento di parse imposta slkUnknown e cancella lo stato derivato, ma il corpo catturato resta in FRawData e il percorso di salvataggio lo preferisce a qualsiasi ricostruzione finché l'elemento non è dirty e non è il record self. L'alternativa — normalizzare un record non analizzato in un auto-riferimento così che il writer abbia qualcosa di ben formato da emettere — converte un record che non avete capito in un record definitivamente sbagliato. Quel principio è lo stesso contratto applicato ai progetti VBA e ai loro riferimenti esterni attraverso un ciclo di caricamento e salvataggio, ed è la differenza tra una libreria che fa il round-trip dei file del mondo reale e una che fa il round-trip dei file che la sua suite di test capita di contenere. Una cartella di lavoro che ha attraversato quindici anni di versioni di Excel, un generatore di report e due strumenti di migrazione conterrà record che nessun vivente ha progettato. Riscriveteli come li avete trovati

La classificazione tipizzata dei record SupBook e XTI è arrivata in HotXLS 2.361.2 fino alla 2.361.4, insieme alla risoluzione XTI limitata e al più sicuro percorso ConvertFormulasToValues descritto qui. Se mantenete codice Delphi o C++Builder che legge file xls legacy con chiamate add-in, link DDE o OLE, o nomi definiti esterni, il componente foglio di calcolo HotXLS per Delphi gestisce l'intera tassonomia in modo nativo, senza installazione di Excel e senza automazione OLE sulla macchina che fa il lavoro