Artigo Técnico

Reescrevendo Código-Fonte VBA e Recomprimindo MS-OVBA em Delphi

Renomear uma referência de planilha fixa no código em mil modelos de relatório habilitados para macro descarta abrir cada arquivo no editor VBA manualmente. O HotXLS, o componente Excel nativo para Delphi e C++Builder, trata esse caso expondo o código-fonte de um módulo VBA como uma propriedade editável SourceCode e recomprimindo cada edição com o algoritmo de compressão MS-OVBA que a Microsoft define para armazenamento VBA, gravando o resultado de volta no armazenamento VBA de um XLS clássico, em um arquivo de projeto VBA autônomo, ou em uma pasta de trabalho XLSM habilitada para macro. Nenhuma instância do Excel, nenhum editor VBA e nenhum gravador de macro está envolvido em nenhum ponto desse caminho

Por que um stream de módulo VBA não é um arquivo de texto

Um módulo VBA dentro de uma pasta de trabalho XLS ou um arquivo de projeto VBA autônomo não é texto-fonte sentado em um stream esperando ser lido — é um pequeno contêiner binário. Um cache de desempenho compilado vem primeiro, os bytes que o Office usa para pular a recompilação do módulo ao carregar quando o cache ainda corresponde à versão do host, e o texto-fonte real vem em seguida, passado por um esquema de compressão proprietário que o MS-OVBA define especificamente para armazenamento VBA. Esse esquema não é zip, não é deflate, e não é nada que as APIs de compressão do Windows produzam nativamente, o que é exatamente por que a maioria das bibliotecas Excel de terceiros consegue ler o código-fonte de um módulo — a descompressão é a metade mais fácil do problema — mas param antes de gravá-lo de volta, já que a recompressão é onde um bit sutilmente errado produz um arquivo que o Excel se recusa a abrir. Existem textos públicos sobre o lado de leitura; implementações do lado de escrita que de fato exercitam a recompressão, em vez de apenas desempacotar um módulo existente para inspeção, são raras o suficiente para que isso permaneça um dos cantos menos documentados dos formatos de arquivo do Excel

O que a propriedade SourceCode do HotXLS realmente muda?

O HotXLS representa cada módulo VBA como um objeto TXLSVBAModule com uma propriedade simples SourceCode: WideString, e atribuir a ela um novo valor é exatamente tão simples quanto parece: o módulo é marcado como sujo em memória, e nada toca o stream OLE subjacente até o projeto ser salvo. O próprio projeto vem de IXLSWorkbook.VBAProject no motor XLS clássico ou TXLSXWorkbook.ParsedVBAProject no motor OOXML habilitado para macro, ambos retornando um TXLSVBAProject cujos módulos ficam atrás de um indexador Item[] baseado em 1 e uma propriedade Count, de modo que uma edição em lote em todos os módulos de uma pasta de trabalho é apenas um loop sobre um intervalo de inteiros

var
  Wb: TXLSWorkbook;
  Project: TXLSVBAProject;
  I: Integer;
  Updated: WideString;
begin
  Wb := TXLSWorkbook.Create;
  try
    Wb.Open('MonthlyReport.xls');
    if Wb.HasVBAProject then
    begin
      Project := Wb.VBAProject;
      for I := 1 to Project.Count do
      begin
        Updated := StringReplace(Project[I].SourceCode,
          'ReportSheet2025', 'ReportSheet2026', [rfReplaceAll]);
        if Updated <> Project[I].SourceCode then
          Project[I].SourceCode := Updated;   // marks the module dirty
      end;
      Wb.SaveAs('MonthlyReport.xls');          // recompresses on write
    end;
  finally
    Wb.Free;
  end;
end;

Esse loop também é a forma de uma passada de auditoria. Antes de mil modelos serem tocados, a maioria das equipes primeiro quer saber quantos deles de fato carregam macros e o que essas macros referenciam, que é o cenário por trás da bancada de auditoria e conversão de pastas de trabalho — o mesmo Project.Count que conduz um loop de reescrita aqui vira uma contagem de macros por arquivo ali

Dentro do contêiner de compressão MS-OVBA

O formato de compressão do MS-OVBA empacota bytes de origem no que a especificação chama de CompressedContainer: um único byte de assinatura, exigido a ser igual a 0x01, seguido de uma sequência de blocos CompressedChunk, cada um cobrindo até 4096 bytes de dados descomprimidos. Um cabeçalho de bloco de 16 bits carrega três campos — uma assinatura de 3 bits que deve ser igual a 3, um campo de tamanho de 12 bits, e um bit CompressedChunkFlag marcando se o payload do bloco é bytes literais ou uma sequência comprimida por token. Quando a flag está definida, o payload é uma sequência de grupos de oito tokens prefixados por um byte de flag, e cada token é ou um único byte literal ou um CopyToken: uma referência retroativa de deslocamento/comprimento a bytes já descomprimidos anteriormente no mesmo bloco, com a largura de bits dividida entre deslocamento e comprimento mudando dependendo de quão fundo no bloco o descompressor está atualmente. Essa parte do MS-OVBA (§2.4.1, Compression and Decompression) é onde uma implementação feita à mão mais frequentemente perde um dia com um erro de um bit no cálculo dessa largura de bits

Por que o HotXLS escreve blocos brutos em vez de casar tokens

O caminho de escrita do HotXLS contorna inteiramente a metade de casamento de tokens desse algoritmo. Quando recomprime um módulo editado, todo bloco sai com o CompressedChunkFlag limpo, significando que o bloco contém bytes literais em vez de tokens de referência retroativa — legal segundo o MS-OVBA, já que um contêiner comprimido pode consistir inteiramente de blocos não comprimidos, e isso remove precisamente a parte do algoritmo mais difícil de acertar à mão: encontrar referências retroativas válidas e empacotar um par deslocamento/comprimento em uma largura de bits que depende da posição atual dentro do bloco. A compensação aparece no tamanho do arquivo, não na correção — um stream de módulo reescrito fica perto do tamanho de seu texto-fonte mais um cabeçalho de dois bytes por bloco de 4096 bytes, não menor da forma que um bloco totalmente comprimido por token seria. Todo leitor que implementa o lado de descompressão da especificação, incluindo o Excel, ainda abre o resultado corretamente, porque um bloco bruto é tão válido quanto um CompressedChunk comprimido por token

O que o HotXLS deixa intocado quando reescreve um módulo

A recompressão só substitui parte do stream do módulo. Todo stream de módulo armazena seu cache de desempenho primeiro e seu código-fonte comprimido em segundo, e o stream dir do projeto registra exatamente onde essa divisão cai para cada módulo em uma entrada MODULEOFFSET; o HotXLS lê esse deslocamento, mantém cada byte antes dele exatamente como encontrou, e reconstrói apenas o contêiner comprimido a partir do deslocamento em diante

O texto-fonte em si faz o percurso de ida e volta através da própria página de código do projeto VBA, em vez de UTF-8 — a mesma página de código legada com a qual o Office escreveu o projeto originalmente. Uma edição de SourceCode que introduz caracteres fora do repertório dessa página de código recebe substituição silenciosa por caracteres de melhor ajuste quando o HotXLS recodifica a string de volta para bytes, não é rejeitada, de modo que um caractere regional incomum solto em um comentário ou literal de string é o lugar mais provável de notar a perda. Referências externas e vinculações de biblioteca dentro do mesmo projeto seguem um caminho de preservação relacionado, mas separado, coberto em o artigo complementar sobre preservação de links externos VBA, e vale a pena ler antes de uma passada de reescrita tocar um projeto que se vincula a outras pastas de trabalho ou bibliotecas de tipo

Como você traz as macros reescritas de volta para uma pasta de trabalho?

Nada chama a etapa de recompressão explicitamente — ela roda automaticamente no instante em que uma pasta de trabalho ou um projeto VBA autônomo é salvo. TXLSVBAProject.ApplyChanges percorre cada módulo, recomprime os que tiveram SourceCode alterado desde o último salvamento, e reescreve apenas o stream desse módulo; o TXLSWorkbook.SaveAs clássico, quando o destino de salvamento mantém o formato original do arquivo, e o TXLSXWorkbook.SaveAs OOXML para um pacote XLSM habilitado para macro, ambos o chamam internamente antes de qualquer coisa ser gravada em disco, e SaveVBAProjectToFile chama o mesmo método quando o alvo é um arquivo de projeto VBA destacado, em vez de uma pasta de trabalho completa

var
  Wb: TXLSWorkbook;
begin
  Wb := TXLSWorkbook.Create;
  try
    if Wb.LoadVBAProjectFromFile('LegacyMacros.ole') = 1 then
    begin
      Wb.VBAProject[1].SourceCode :=
        StringReplace(Wb.VBAProject[1].SourceCode, 'OldServer', 'NewServer', [rfReplaceAll]);
      Wb.SaveVBAProjectToFile('LegacyMacros_Patched.ole');  // ApplyChanges runs internally
    end;
  finally
    Wb.Free;
  end;
end;
var
  Xlsx: TXLSXWorkbook;
  Project: TXLSVBAProject;
begin
  Xlsx := TXLSXWorkbook.Create;
  try
    Xlsx.Open('Dashboard.xlsm');
    Project := Xlsx.ParsedVBAProject;
    if Assigned(Project) then
    begin
      Project[1].SourceCode := StringReplace(Project[1].SourceCode,
        'ConnStringV1', 'ConnStringV2', [rfReplaceAll]);
      Xlsx.SaveAs('Dashboard.xlsm');   // SyncParsedVBAProject recompresses before the part is written
    end;
  finally
    Xlsx.Free;
  end;
end;

Todos os três destinos compartilham a mesma mecânica de SourceCode e ApplyChanges por baixo; a única diferença real entre eles é qual chamada de salvamento acaba disparando a recompressão

Onde isso ainda quebra

Dois modos de falha são comuns o suficiente para planejar antes de uma passada de reescrita rodar contra arquivos de produção. Um projeto VBA assinado digitalmente deixa de estar validamente assinado no instante em que seu código-fonte muda, já que a assinatura cobre o conteúdo do projeto; o HotXLS não tem como reassinar um projeto em seu nome, e o Excel descarta ou sinaliza a assinatura na próxima vez que o arquivo abre, de modo que um projeto de macro assinado precisa de uma etapa de reassinatura downstream se essa assinatura for algo que seu fluxo de trabalho de fato verifica. O segundo modo de falha pertence a qualquer um tentado a reimplementar esse formato de compressão do zero em vez de usar uma biblioteca que já o trata: um único bit errado em um cabeçalho de bloco, na quadra de assinatura, no campo de tamanho, ou na flag de compressão, produz um arquivo que o Excel se recusa a abrir, geralmente atrás de um aviso genérico de corrupção que não dá nenhuma dica de qual byte estava errado — precisamente a classe de bug que a estratégia de escrita de blocos brutos descrita antes existe para evitar

Nada disso exige fazer engenharia reversa do formato para usar. Desenvolvedores Delphi e C++Builder recebem acesso de leitura e escrita a SourceCode, recompressão em conformidade com MS-OVBA, e todos os três destinos de gravação descritos aqui como parte do Componente HotXLS padrão, ao lado do restante de sua API de pasta de trabalho XLS clássica e OOXML