Artigo Técnico

Classificação de links externos SupBook e XTI em Delphi

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

A escada dirigida por sentinela que o HotXLS usa para classificar um record SupBook do BIFF em sete tipos, decodificando uma string só para valores no intervalo de caminho codificado e caindo num tipo desconhecido em vez de num branch padrão
Cada tipo é alcançado por uma sentinela em vez de um teste de string, e um record que não casa com nenhuma das formas permanece desconhecido em vez de cair num branch padrão que significa esta pasta de trabalho

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 o HotXLS lê o primeiro code point bruto do corpo de um record SupBook do BIFF em vez da string decodificada, porque o leitor de strings de uso geral transforma o marcador same-sheet U+0000 num valor vazio
O marcador same-sheet é uma string de um caractere cujo caractere é U+0000, então o leitor geral de strings o dobra num valor vazio e só o code point bruto no offset do byte de opção o mantém

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

O HotXLS convertendo o índice XTI base zero de um token PtgNameX do BIFF em seu ExternID interno base um num único ponto, com resolução limitada nas duas pontas e o mapa de classificação que o consome
O off-by-one entre o índice em disco base zero e o ExternID interno base um é aplicado uma vez, quando um token entra na árvore de sintaxe, e todo índice não resolvível aterrissa na classe malformada

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