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
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é 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
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