Abra um xls antigo, salve-o de novo, e a fórmula add-in que chamava uma biblioteca de análise registrada agora aponta para uma referência vazia dentro da própria pasta de trabalho. O HotXLS rastreia essa corrupção silenciosa até uma suposição ruim: que um record SupBook do BIFF é ou self ou um arquivo externo. O [MS-XLS] define sete tipos, não dois
Por que uma pasta de trabalho salva perde seus links add-in?
Porque o teste de classificação era estrutural em vez de tipado. O atalho tradicional lê um record SupBook ($01AE), checa se ele carrega o marcador self e, se não, trata qualquer string seguinte como uma URL de documento. Todo record que não é nenhuma dessas duas coisas cai num branch padrão, e o branch padrão quase sempre é "esta é a própria pasta de trabalho". Um link add-in, um link same-sheet, um slot não usado e um record truncado acabam todos vestindo o mesmo rótulo errado. Nada lança exceção enquanto isso acontece: o record fez parse, a fórmula recompilou, o arquivo foi salvo sem aviso, e o defeito aparece três semanas depois quando alguém nota uma coluna de zeros onde antes havia uma conversão de moeda. O [MS-XLS] §2.4.271 descreve um record que pode ser uma autorreferência, uma referência same-sheet, um contêiner de função add-in, uma pasta de trabalho externa com caminho virtual e tabela de nomes de planilha, um link de dados DDE ou OLE, ou um placeholder não usado — e um sétimo estado que não está na especificação mas existe em discos reais, o record que não faz parse. A correção não é uma heurística melhor; é se recusar a ter uma heurística
Os sete tipos que um record SupBook pode carregar
O HotXLS declara a taxonomia de supporting links como uma enumeração fechada em lxExternSheet.pas, e toda decisão a jusante comuta 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 no parse, ou bytes restantes permaneceram
slkSelf, // esta pasta de trabalho
slkSameSheet, // marcador U+0000
slkAddIn, // contêiner de função add-in
slkExternalWorkbook, // caminho virtual + tabela de nomes de planilha
slkDde, // resolvido pelas flags de ExternName
slkOle, // resolvido pelas flags de ExternName
slkDdeOrOle, // um dos dois, ainda não se sabe qual
slkUnused); // placeholder de espaço único
TXLSFormulaReferenceClass = (
frcInternal,
frcExternalWorkbook,
frcExternalOther,
frcUnknownOrMalformed);
TXLSXtiInfo = record
XtiIndex : Integer; // base zero, como armazenado em ExternSheet.rgXTI
ExternID : Integer; // base um, a convenção interna
SupBookIndex: Integer;
Sheet1Index : Integer;
Sheet2Index : Integer;
LinkKind : TXLSSupportingLinkKind;
end;
O dispatch é dirigido por sentinela, não por string. Um valor de campo $0401 marca o record self. Uma contagem de planilhas de um pareado com $3A01 marca um contêiner add-in. Só um valor no intervalo de 1 a $00FF significa que um caminho virtual codificado segue, e só então o HotXLS decodifica uma string. Qualquer coisa fora dessas três formas permanece slkUnknown, e um record cuja tabela de nomes de planilha não consuma o corpo do record exatamente é rebaixado de volta a slkUnknown mesmo quando o cabeçalho parecia plausível
Por que o marcador same-sheet decodifica como string vazia?
Porque o leitor de strings BIFF de uso geral destrói o byte de que a classificação depende. O supporting link same-sheet é uma string de um caractere cujo único caractere é 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 code point bruto do corpo do record em vez de confiar no valor decodificado:
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); // wide, 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 branch comprimido-versus-wide. O byte de opção fica num offset fixo do header da string e o primeiro code point é um ou dois bytes dependendo do bit 0, então lê-lo como byte incondicionalmente funciona na maioria dos arquivos e falha nos escritos por builds localizadas — a pior distribuição possível para um bug. O placeholder não usado é pego do mesmo jeito, por seu payload literal de espaço único, e o caso DDE ou OLE pelo separador U+0003 embutido no caminho codificado
Por que DDE e OLE não podem ser separados no momento do SupBook?
Porque o record SupBook não carrega os bits distintivos. Ele diz que o link é um dos dois; as flags fOle e fOleLink que decidem qual moram no record ExternName ($0023) que chega mais tarde no stream. O HotXLS grava slkDdeOrOle no momento do parse e o estreita em ParseExternalName, e se nenhum ExternName jamais chega o tipo permanece provisório para sempre — o que é correto, porque o arquivo genuinamente não diz. Todo consumidor a jusante trata esse valor provisório como um valor real em vez de ausente, então nenhum chamador precisa inventar um desempate. Chutar "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;
Índices XTI são base zero no disco e base um por dentro
O HotXLS faz a conversão de off-by-one exatamente uma vez, no ponto em que um token entra na árvore de sintaxe interna, e em nenhum outro lugar. O PtgNameX.ixti ([MS-XLS] §2.5.198.85) é um índice base zero no array rgXTI do record ExternSheet ($0017, §2.4.106), enquanto a convenção interna de ExternID da biblioteca é base um com zero reservado para "nenhuma planilha externa". O caminho de leitura BIFF8 faz FExternID := wValue + 1 quando decodifica um token tNameX e o caminho de escrita emite StoreExternID - 1, deixando a visão bruta do token e a semântica em disco intocadas. Errar isso é incomumente difícil de pegar: nomes definidos externos resolvem para a entrada vizinha, e num arquivo com uma única entrada XTI o índice 0 se torna índice 1, erra, e o nome degrada silenciosamente. Uma regressão que só exercite texto de fórmula recompilado nunca o vê, porque recompilação nunca toca no índice em disco — a mesma armadilha que torna nomes definidos abrangendo planilhas e pastas de trabalho dignos de teste contra streams de bytes reais. A resolução é limitada nas duas pontas: TlxExternSheetSheet.TryResolveXti retorna False para um índice negativo ou uma entrada faltando, TXLSSupBook.TryGetKind retorna False para um índice SupBook fora do array, e ClassifyXti então mapeia slkSelf e slkSameSheet para frcInternal, slkExternalWorkbook para frcExternalWorkbook, e slkAddIn, slkDde, slkOle e slkDdeOrOle para frcExternalOther. Todo o resto, incluindo todo caminho fora de range, aterrissa em frcUnknownOrMalformed
Classificar uma fórmula antes de congelá-la
O TXLSCompiledFormula.ClassifyReferences escaneia o stream de tokens BIFF preservado diretamente em vez de descompilar a fórmula e procurar colchetes. Caça a colchetes em texto de fórmula é uma heurística de texto vestindo casaco de parser: casa com literais de string, casa com referências estruturadas e erra completamente nomes definidos externos, já que estes não carregam colchetes na forma descompilada. O escaneio de tokens olha apenas PtgNameX, PtgRef3d, PtgArea3d, PtgRefErr3d e PtgAreaErr3d, caindo para uma caminhada na árvore de sintaxe quando nenhum stream BIFF sobrevive. A mesclagem é deliberadamente pessimista — a prioridade fixa é frcUnknownOrMalformed, depois frcExternalWorkbook, depois frcExternalOther, depois frcInternal — então um único token ilegível envenena a fórmula inteira. Para um nome definido externo o índice do nome também é validado: base um, em range, e respaldado por um record 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 SOMENTE fórmulas classificadas frcExternalWorkbook;
// referências internas, add-in, DDE/OLE e malformadas permanecem 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. Congelar uma fórmula é irreversível, então a operação precisa provar que uma referência é uma pasta de trabalho externa em vez de meramente suspeitar. Chamadas add-in sobrevivem, links DDE e OLE sobrevivem, e qualquer coisa que o parser não conseguiu entender totalmente sobrevive, porque o desfecho seguro da incerteza é não mudar nada. A mesma disciplina governa o rebinding de fórmulas copiadas entre pastas de trabalho, onde uma referência malclassificada rebinda para a pasta errada em vez de falhar ruidosamente
Records que não fazem parse são escritos de volta intocados
O HotXLS mantém o payload original do SupBook e o reemite byte a byte quando o record nunca foi editado. Uma falha de parse define slkUnknown e limpa o estado derivado, mas o corpo capturado permanece em FRawData e o caminho de gravação o prefere sobre qualquer reconstrução enquanto o item não está sujo e não é o record self. A alternativa — normalizar um record não parseado numa autorreferência para que o writer tenha algo bem-formado para emitir — converte um record que você não entendeu num record definitivamente errado. Esse princípio é o mesmo contrato aplicado a projetos VBA e suas referências externas através de um ciclo load-and-save, e é a diferença entre uma biblioteca que faz round-trip de arquivos do mundo real e uma que faz round-trip dos arquivos que sua suíte de testes por acaso contém. Uma pasta de trabalho que passou por quinze anos de versões do Excel, um gerador de relatórios e duas ferramentas de migração conterá records que ninguém vivo hoje desenhou. Escreva-os de volta como os encontrou
A classificação tipada de records SupBook e XTI chegou no HotXLS 2.361.2 até 2.361.4, junto com resolução XTI limitada e o caminho mais seguro de ConvertFormulasToValues descrito aqui. Se você mantém código Delphi ou C++Builder que lê arquivos xls legados carregando chamadas add-in, links DDE ou OLE, ou nomes definidos externos, o HotXLS Delphi spreadsheet component trata a taxonomia inteira nativamente, sem instalação de Excel e sem automação OLE na máquina que faz o trabalho