Abra um xls antigo, guarde-o outra vez, e a fórmula de add-in que chamava uma biblioteca de análise registada passa a apontar para uma referência vazia dentro do próprio livro. O HotXLS atribui essa corrupção silenciosa a uma suposição errada: a de que um registo BIFF SupBook é ou self ou um ficheiro externo. O [MS-XLS] define sete tipos, não dois
Porque é que um livro guardado perde as suas ligações de add-in?
Porque o teste de classificação era estrutural em vez de tipado. O atalho tradicional lê um registo SupBook ($01AE), verifica se transporta o marcador self e, caso não transporte, trata a cadeia que se segue como um URL de documento. Todos os registos que não são nenhuma dessas duas coisas caem num ramo por omissão, e o ramo por omissão é quase sempre «este é o próprio livro». Uma ligação de suporte de add-in, uma ligação same-sheet, uma ranhura não usada e um registo truncado acabam todos com a mesma etiqueta errada. Nada lança exceções enquanto isto acontece: o registo foi processado, a fórmula recompilada, o ficheiro guardado sem aviso, e o defeito aparece três semanas depois quando alguém nota uma coluna de zeros onde estava uma conversão de moeda. O [MS-XLS] §2.4.271 descreve um registo que pode ser uma autorreferência, uma referência same-sheet, um contentor de funções de add-in, um livro externo com um caminho virtual e uma tabela de nomes de folhas, uma ligação de dados DDE ou OLE, ou um marcador de posição não usado — e um sétimo estado que não está na especificação mas existe em discos reais, o registo que não analisa. A correção não é uma heurística melhor; é recusar ter uma heurística
Os sete tipos que um registo SupBook pode transportar
O HotXLS declara a taxonomia de ligações de suporte como uma enumeração fechada em lxExternSheet.pas, e todas as decisões a jusante comutam sobre ela. Nove valores de enumeração cobrem as sete categorias, porque o caso DDE e OLE precisa de um estado provisório antes de poder ser resolvido:
type
TXLSSupportingLinkKind = (
slkUnknown, // falhou a análise, ou restaram bytes extra no final
slkSelf, // este livro
slkSameSheet, // marcador U+0000
slkAddIn, // contentor de funções de add-in
slkExternalWorkbook, // caminho virtual + tabela de nomes de folhas
slkDde, // resolvido a partir das flags de ExternName
slkOle, // resolvido a partir das flags de ExternName
slkDdeOrOle, // um dos dois, ainda sem se saber qual
slkUnused); // marcador de um único espaço
TXLSFormulaReferenceClass = (
frcInternal,
frcExternalWorkbook,
frcExternalOther,
frcUnknownOrMalformed);
TXLSXtiInfo = record
XtiIndex : Integer; // base zero, tal como armazenado em ExternSheet.rgXTI
ExternID : Integer; // base um, a convenção interna
SupBookIndex: Integer;
Sheet1Index : Integer;
Sheet2Index : Integer;
LinkKind : TXLSSupportingLinkKind;
end;
O despacho é orientado por sentinelas, não orientado por cadeias. Um valor de campo de $0401 marca o registo self. Uma contagem de folhas de um emparelhada com $3A01 marca um contentor de add-in. Só um valor no intervalo de 1 a $00FF significa que se segue um caminho virtual codificado, e só então o HotXLS descodifica uma cadeia. Tudo o que esteja fora dessas três formas permanece slkUnknown, e um registo cuja tabela de nomes de folhas não consuma exatamente o corpo do registo é rebaixado de volta para slkUnknown mesmo quando o cabeçalho parecia plausível
Porque é que o marcador same-sheet descodifica como uma cadeia vazia?
Porque o leitor de cadeias BIFF de uso geral destrói o byte de que a classificação depende. A ligação de suporte same-sheet é uma cadeia de um carácter cujo único carácter é U+0000, e o TXLSBlob.GetBiffString devolve isso como um WideString vazio, indistinguível de um caminho genuinamente vazio — que é exatamente a entrada a que uma heurística de autorreferência responde «self». O HotXLS por isso lê o primeiro ponto de código em bruto do corpo do registo em vez de confiar no valor descodificado:
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) // comprimido, um byte
else
FirstChar := Data.GetWord(StringOffset + 3); // largo, dois 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;
Note o ramo comprimido contra largo. O byte de opções está a um desvio fixo do cabeçalho da cadeia e o primeiro ponto de código tem um byte ou dois consoante o bit 0, pelo que lê-lo como um byte incondicionalmente funciona na maioria dos ficheiros e falha nos escritos por compilações localizadas — a pior distribuição possível para um erro. O marcador de posição não usado é apanhado da mesma maneira, pela sua carga literal de um único espaço, e o caso DDE ou OLE pelo separador U+0003 embutido no caminho codificado
Porque é que DDE e OLE não podem ser separados no momento do SupBook?
Porque o registo SupBook não transporta os bits distintivos. Ele diz-lhe que a ligação é um dos dois; as flags fOle e fOleLink que decidem qual vivem no registo ExternName ($0023) que chega mais adiante no fluxo. O HotXLS regista slkDdeOrOle no momento da análise e restringe-o em ParseExternalName, e se nenhum ExternName chegar o tipo permanece provisório para sempre — o que está correto, porque o ficheiro genuinamente não o diz. Todos os consumidores a jusante tratam esse valor provisório como um valor real em vez de um em falta, pelo que nenhum chamador tem de inventar um desempate. Adivinhar «provavelmente DDE» aqui compraria uma enumeração mais arrumada e uma classe de respostas erradas que ninguém conseguiria rastrear:
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;
Os índices XTI são base zero no disco e base um lá dentro
O HotXLS executa a conversão de erro de um exatamente uma vez, no ponto em que um token entra na árvore de sintaxe interna, e em mais lado nenhum. PtgNameX.ixti ([MS-XLS] §2.5.198.85) é um índice base zero no array rgXTI do registo ExternSheet ($0017, §2.4.106), enquanto a convenção interna de ExternID da biblioteca é base um com zero reservado para «sem folha externa». O caminho de leitura BIFF8 faz FExternID := wValue + 1 quando descodifica um token tNameX e o caminho de escrita emite StoreExternID - 1, deixando a vista em bruto dos tokens e a semântica em disco intactas. Errar isto é invulgarmente difícil de apanhar: os nomes definidos externos resolvem para a entrada vizinha, e num ficheiro com uma única entrada XTI o índice 0 torna-se índice 1, falha, e o nome degrada em silêncio. Uma regressão que só exercite texto de fórmulas recompiladas nunca o vê, porque a recompilação nunca toca no índice em disco — a mesma armadilha que torna os nomes definidos que atravessam folhas e livros merecedores de teste contra fluxos de bytes reais. A resolução é limitada em ambas as pontas: TlxExternSheetSheet.TryResolveXti devolve False para um índice negativo ou uma entrada em falta, TXLSSupBook.TryGetKind devolve False para um índice SupBook fora do array, e o ClassifyXti mapeia depois slkSelf e slkSameSheet para frcInternal, slkExternalWorkbook para frcExternalWorkbook, e slkAddIn, slkDde, slkOle e slkDdeOrOle para frcExternalOther. Todo o resto, incluindo todos os caminhos fora do intervalo, cai em frcUnknownOrMalformed
Classificar uma fórmula antes de a congelar
O TXLSCompiledFormula.ClassifyReferences analisa diretamente o fluxo de tokens BIFF preservado em vez de descompilar a fórmula e procurar parênteses retas. Caçar parênteses retas no texto da fórmula é uma heurística de texto com um casaco de analisador: corresponde a literais de cadeia, corresponde a referências estruturadas, e perde completamente os nomes definidos externos, já que estes não transportam parênteses retas na forma descompilada. A varredura de tokens olha apenas para PtgNameX, PtgRef3d, PtgArea3d, PtgRefErr3d e PtgAreaErr3d, recorrendo a um percurso da árvore de sintaxe quando nenhum fluxo BIFF sobrevive. A fusão é deliberadamente pessimista — a prioridade fixa é frcUnknownOrMalformed, depois frcExternalWorkbook, depois frcExternalOther, depois frcInternal — pelo que um único token ilegível envenena a fórmula inteira. Para um nome definido externo o índice do nome também é validado: base um, dentro do intervalo, e apoiado por um registo ExternName retido
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 é base um
begin
Sheet := Wb.Sheets[i];
// congela APENAS fórmulas classificadas frcExternalWorkbook;
// referências internas, de add-in, DDE/OLE e malformadas continuam fórmulas
Sheet.ConvertFormulasToValues(True);
end;
Wb.SaveAs('quarterly-detached.xls');
finally
Wb.Free;
end;
end;
O parâmetro OnlyExternal é onde a taxonomia se paga a si própria. Congelar uma fórmula é irreversível, pelo que a operação tem de provar que uma referência é um livro externo em vez de meramente suspeitar disso. As chamadas de add-in sobrevivem, as ligações DDE e OLE sobrevivem, e tudo o que o analisador não conseguiu compreender totalmente sobrevive, porque o resultado seguro da incerteza é não mudar nada. A mesma disciplina governa o religamento de fórmulas copiadas entre livros, onde uma referência mal classificada religa ao livro errado em vez de falhar ruidosamente
Os registos que não analisam são escritos de volta intactos
O HotXLS mantém a carga útil original do SupBook e reemite-a byte a byte quando o registo nunca foi editado. Uma falha de análise define slkUnknown e limpa o estado derivado, mas o corpo capturado permanece em FRawData e o caminho de gravação prefere-o a qualquer reconstrução desde que o item não esteja marcado como alterado e não seja o registo self. A alternativa — normalizar um registo não analisado numa autorreferência para que o escritor tenha algo bem formado para emitir — converte um registo que não compreendeu num registo que está definitivamente errado. Esse princípio é o mesmo contrato aplicado aos projetos VBA e às suas referências externas ao longo de um ciclo de carregar e guardar, e é a diferença entre uma biblioteca que faz a viagem de ida e volta de ficheiros reais e uma que faz a viagem de ida e volta dos ficheiros que a sua suite de testes por acaso contém. Um livro que passou por quinze anos de versões do Excel, um gerador de relatórios e duas ferramentas de migração conterá registos que ninguém hoje vivo desenhou. Escreva-os de volta como os encontrou
A classificação tipada de registos SupBook e XTI chegou no HotXLS 2.361.2 a 2.361.4, juntamente com a resolução XTI limitada e o caminho ConvertFormulasToValues mais seguro aqui descrito. Se mantém código Delphi ou C++Builder que lê ficheiros xls legados com chamadas de add-in, ligações DDE ou OLE, ou nomes definidos externos, o componente de folhas de cálculo HotXLS para Delphi trata de toda a taxonomia nativamente, sem instalação do Excel e sem automação OLE na máquina que faz o trabalho