Artigo Técnico

XLUnicodeString BIFF8 no Delphi: cch e fHigh

O HotXLS decodifica uma XLUnicodeString BIFF8 lendo primeiro cch e a flag fHigh, depois escolhendo o reader compatível com a codificação: TXLSBlob.GetWideString com uma contagem de bytes de cch * 2 quando fHigh é 1, e TXLSBlob.GetString quando fHigh é 0. Inverta esse pareamento e o registro produzirá uma string vazia ou com metade do tamanho, nunca uma exceção

É isso que torna essa classe de bug cara. Um gráfico abre, as séries são desenhadas corretamente, os eixos estão certos e uma legenda de trendline simplesmente fica em branco. Nada no log, nada no exception handler, nenhum diálogo de arquivo corrompido. O arquivo estava correto o tempo todo; o reader pediu a quantidade errada de bytes e recebeu exatamente o que pediu

Por que uma string BIFF8 volta vazia?

Uma string BIFF8 volta vazia porque uma proteção de tamanho rejeitou o payload antes de a leitura acontecer, ou porque o reader parou no primeiro NUL encontrado. Os dois caminhos são silenciosos por construção. No HotXLS, a proteção costuma ser uma checagem explícita de DataLength no handler do registro e precisa ser calculada por codificação: um payload de 16 bits precisa de 8 + cch * 2 bytes para um corpo SXViewLink, mas um payload de 8 bits precisa apenas de 8 + cch. Aplicar a aritmética de caracteres largos a um registro de 8 bits faz todo nome curto falhar na barreira. O comportamento de NUL é a segunda armadilha, porque TXLSBlob.GetString e TXLSBlob.GetWideString procuram um terminador no resultado decodificado e truncam ali, retornando uma string vazia quando o terminador cai na posição um. Ler um corpo de 16 bits com metade da contagem de bytes mantém os primeiros cch div 2 caracteres; ler um corpo de 8 bits pelo reader wide forma code points arbitrários a partir dos pares de bytes. Somente uma leitura longa é ruidosa: TXLSBlob.EnsureReadable levanta Blob read exceeds data size quando a solicitação passa do tamanho do blob. Ler de menos não tem esse alarme

GetWideString conta bytes, não caracteres

TXLSBlob.GetWideString(Index, Count) recebe Count em bytes. Internamente ele executa um SetString sobre um PWideChar com Count div SizeOf(WideChar), então passar uma contagem de caracteres silenciosamente corta a string pela metade. Os layouts de registros BIFF8, enquanto isso, expressam o comprimento da string em caracteres. Toda chamada de 16 bits precisa carregar a conversão * 2 por conta própria, e toda chamada de 8 bits não pode carregá-la. Essa é a mesma fronteira de codificação que aparece ao gravar texto de volta, em vez de lê-lo, e vale consultar junto com a exportação de planilhas Unicode-safe no Delphi se seu pipeline move strings nos dois sentidos

// XLUnicodeStringNoCch de 16 bits: cch caracteres, cch * 2 bytes
Name := Data.GetWideString(Start, cch * 2);        // correto
Name := Data.GetWideString(Start, cch);            // metade do texto, sem erro

// XLUnicodeStringNoCch de 8 bits: cch caracteres, cch bytes
Name := WideString(Data.GetString(Start, cch));    // correto
Name := Data.GetWideStringWithZero(Start, cch);    // ainda e um reader wide

A convenção vale em todo lugar em que o stream de bytes é percorrido à mão. Quando o HotXLS junta um registro String longo ($0207, [MS-XLS] 2.4.268) a partir de seus registros Continue ($003C), o branch wide calcula segCh a partir do tamanho do segmento e então chama GetWideString(3, segCh * 2), porque o corpo do registro começa no offset 3 e a contagem continua sendo de bytes. O reader de rich text faz a mesma coisa a partir do offset 1 no primeiro segmento Continue. [MS-XLS] 2.5.293 garante que a quebra fique em um limite de caractere de dois bytes quando fHighByte é 1, portanto não é necessário controle de caractere parcial, mas a aritmética de bytes continua sendo responsabilidade sua

O que GetWideStringWithZero realmente faz?

TXLSBlob.GetWideStringWithZero é um reader de caracteres wide que preserva NULs incorporados. O sufixo WithZero indica retenção de NUL, não largura de caractere: internamente ele executa o mesmo SetString sobre um PWideChar com Count div SizeOf(WideChar) de GetWideString, mas sem a busca pelo terminador. O equivalente de um byte é TXLSBlob.GetStringWithZero, que retorna uma AnsiString. O nome não diz qual é qual, e essa ambiguidade já custou bugs reais a este codebase. Vale nomear o erro específico, porque ele parece plausível: GetWideString precisa de cch * 2, então GetWideStringWithZero deve ser o que recebe cch diretamente. Ele recebe cch sem reclamar, retorna uma WideString e o compilador fica satisfeito. Também retorna metade dos caracteres, montados a partir dos pares de bytes errados. O caminho correto de 8 bits é TXLSBlob.GetString com uma contagem de bytes cch simples, convertida para WideString na atribuição. O HotXLS 2.376.0 corrigiu exatamente esse uso incorreto em dois decoders de gráfico

SXViewLink e a barreira de tamanho por codificação

SXViewLink ($0858, [MS-XLS] 2.4.316) é o exemplo trabalhado mais claro, pois reúne as duas assimetrias em um cabeçalho de oito bytes. O layout é rt(2), unused(2), reserved(2), cch(1), fHigh(1), seguido de um corpo XLUnicodeStringNoCch: fHigh = 1 significa cch * 2 bytes de UTF-16, fHigh = 0 significa cch bytes de caracteres de um byte, e cch é limitado a 255 porque o campo de tamanho tem um único byte. O HotXLS grava o registro nos chart globals antes de Units, ao lado de PivotChartBits ($0859, [MS-XLS] 2.4.196), quando uma chart sheet aponta para uma view de PivotTable; a visão no nível de registros dessa maquinaria está coberta em gravar registros de PivotTable BIFF8 no Delphi

// SXViewLink ([MS-XLS] 2.4.316): rt(2) unused(2) reserved(2) cch(1)
// depois um XLUnicodeStringNoCch - fHigh(1) seguido pelos caracteres
PivCch := Item.FData.GetByte(6);
if (PivCch > 0) and (Item.FData.GetByte(7) <> 0) and
   (Item.FData.DataLength >= LongWord(8 + PivCch * 2)) then
begin
  Result.PivotSourceName := Item.FData.GetWideString(8, PivCch * 2);
  Result.IsPivotChart := True;
end
else if (PivCch > 0) and (Item.FData.GetByte(7) = 0) and
   (Item.FData.DataLength >= LongWord(8 + PivCch)) then
begin
  Result.PivotSourceName := WideString(Item.FData.GetString(8, PivCch));
  Result.IsPivotChart := True;
end;

Os dois branches não são cosméticos. Uma versão anterior protegia as duas codificações atrás da expressão wide 8 + cch * 2, então um nome de view de 8 bits gravado pelo Excel falhava na barreira e o decoder retornava uma PivotSourceName vazia com IsPivotChart mantido como false. O link do pivô desaparecia do modelo sem um único diagnóstico. O mesmo erro estava no decoder de Trendline ($2050, [MS-XLS] 2.4.328), em que o campo de nome segue 28 bytes de payload numérico, com um cch de dois bytes no offset 28, fHigh no offset 30 e os caracteres no offset 31; legendas de trendline que o Excel havia gravado com caracteres de 8 bits eram decodificadas como strings vazias. Ambos foram corrigidos na mesma release. E o caso de 8 bits não é uma curiosidade legada confinada a arquivos do Excel 2.0 a 4.0: o Excel atual ainda grava payloads BIFF8 de 8 bits sempre que todos os caracteres cabem em um byte

Como decodificar com segurança um novo registro BIFF8 no Delphi?

Quando o campo é realmente uma XLUnicodeString padrão, use TXLSBlob.GetBiffString em vez de escrever o branch à mão. Ele lê o campo de comprimento, lê o byte de opções, encaminha para o reader compatível e avança o cursor além do corpo. Os dois parâmetros Booleanos são a parte que merece atenção: is8bit descreve a largura do campo de comprimento, não a largura dos caracteres, e iswide informa se existe um byte de opção fHigh. Versões BIFF abaixo de $0600 não têm nenhum dos dois

var
  Offset: LongWord;
begin
  Offset := 6;  // em SXViewLink o byte cch começa aqui
  // is8bit = o campo de comprimento tem um byte de largura
  // iswide = um byte de opcao fHigh segue o campo de comprimento
  Name := Data.GetBiffString(Offset, True, True);
  // Offset agora aponta para o primeiro byte depois do corpo da string

Branches escritos à mão ainda têm lugar quando o handler precisa sobreviver a uma entrada truncada ou hostil, porque GetBiffString depende de EnsureReadable levantar uma exceção, em vez de uma checagem de limites que você controla. É por isso que o decoder de gráficos do HotXLS verifica DataLength e não retorna nada em vez de lançar: um workbook de terceiros malformado deveria custar uma legenda, não o documento inteiro. A troca é deliberada e é exatamente por isso que a barreira específica da codificação precisa estar correta, pois é ela que converte uma leitura ruim em silêncio

Uma última parte do processo, aprendida da forma difícil na mesma release. Faça asserção sobre um workbook salvo e reaberto, não sobre o modelo em memória que você acabou de construir. O batch 2.376.0 também encontrou um emitter SXEx ([MS-XLS] 2.4.282) que declarava um corpo de 24 bytes e gravava apenas 22, desalinhando todo registro depois da view de PivotTable, incluindo o EOF da worksheet e qualquer substream de chart sheet que viesse depois. Os testes de pivô existentes nunca capturaram isso porque todos afirmavam contra a memória. A decodificação de strings tem a mesma propriedade: um round-trip pelo arquivo é o único teste que realmente exercita as contagens de bytes

Se você trabalha com internals de XLS clássico em Delphi ou C++Builder e prefere não manter seu próprio reader de registros BIFF8, as regras de codificação acima já estão implementadas e cobertas por regressões no componente de planilhas Delphi HotXLS, que lê e grava XLS e XLSX sem Excel nem automação OLE