Artigo Técnico

Classificar ligações externas SupBook e XTI em Delphi

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

A escada orientada por sentinelas que o HotXLS usa para classificar um registo BIFF SupBook em sete tipos, descodificando uma cadeia só para valores no intervalo de caminho codificado e recuando para um tipo desconhecido em vez de um ramo por omissão
Cada tipo é alcançado por uma sentinela em vez de um teste de cadeia, e um registo que não corresponda a nenhuma das formas permanece desconhecido em vez de cair num ramo por omissão que significa este livro

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 o HotXLS lê o primeiro ponto de código em bruto do corpo de um registo BIFF SupBook em vez da cadeia descodificada, porque o leitor de cadeias de uso geral transforma o marcador same-sheet U+0000 num valor vazio
O marcador same-sheet é uma cadeia de um carácter cujo carácter é U+0000, pelo que o leitor de cadeias geral funde-o num valor vazio e só o ponto de código em bruto no desvio do byte de opções o preserva

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

O HotXLS a converter o índice XTI base zero de um token BIFF PtgNameX no seu ExternID interno base um num único ponto, com resolução limitada em ambas as pontas e o mapa de classificação que o consome
O erro de um 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 todos os índices irresolvíveis caem na classe malformada

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