Artículo técnico

Reescribir código fuente VBA y recomprimir con MS-OVBA en Delphi

Renombrar una referencia de hoja de cálculo fija a través de mil plantillas de informe habilitadas para macros descarta abrir cada archivo a mano en el editor de VBA. HotXLS, el componente Excel nativo para Delphi y C++Builder, resuelve ese caso exponiendo el código fuente de un módulo VBA como una propiedad SourceCode editable y recomprimiendo cada edición con el algoritmo de compresión MS-OVBA que Microsoft define para el almacenamiento de VBA, escribiendo el resultado de vuelta en el almacenamiento VBA de un XLS clásico, en un archivo de proyecto VBA independiente o en un libro XLSM habilitado para macros. Ninguna instancia de Excel, ningún editor de VBA y ninguna grabadora de macros interviene en ningún punto de esa vía

Por qué un flujo de módulo VBA no es un archivo de texto

Un módulo VBA dentro de un libro XLS o de un archivo de proyecto VBA independiente no es texto fuente alojado en un flujo esperando a ser leído, es un pequeño contenedor binario. Primero va una caché de rendimiento compilada, los bytes que Office usa para saltarse la recompilación del módulo al cargar cuando la caché todavía coincide con la versión del host, y a continuación va el texto fuente real, pasado por un esquema de compresión propietario que MS-OVBA define específicamente para el almacenamiento de VBA. Ese esquema no es zip, no es deflate, y no es nada que las API de compresión de Windows produzcan de forma nativa, que es exactamente por qué la mayoría de las bibliotecas de Excel de terceros pueden leer el código fuente de un módulo, la descompresión es la mitad más fácil del problema, pero se quedan cortas a la hora de volver a escribirlo, ya que la recompresión es donde un bit sutilmente equivocado produce un archivo que Excel se niega a abrir. Existen artículos públicos sobre el lado de lectura; las implementaciones del lado de escritura que realmente ejercitan la recompresión, en lugar de limitarse a desempaquetar un módulo existente para inspección, son lo bastante escasas como para que esto siga siendo uno de los rincones peor documentados de los formatos de archivo de Excel

¿Qué cambia realmente la propiedad SourceCode de HotXLS?

HotXLS representa cada módulo VBA como un objeto TXLSVBAModule con una propiedad sencilla SourceCode: WideString, y asignarle un valor nuevo es exactamente tan simple como parece: el módulo se marca como modificado en memoria, y nada toca el flujo OLE subyacente hasta que se guarda el proyecto. El propio proyecto procede de IXLSWorkbook.VBAProject en el motor XLS clásico o de TXLSXWorkbook.ParsedVBAProject en el motor OOXML habilitado para macros, ambos devolviendo un TXLSVBAProject cuyos módulos se sitúan detrás de un indexador Item[] basado en 1 y una propiedad Count, así que una edición por lotes a través de cada módulo de un libro es simplemente un bucle sobre un rango de enteros

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;

Ese bucle es también la forma de una pasada de auditoría. Antes de tocar mil plantillas, la mayoría de los equipos primero quieren saber cuántas de ellas realmente llevan macros y a qué hacen referencia esas macros, que es el escenario que hay detrás del banco de auditoría y conversión de libros, el mismo Project.Count que gobierna un bucle de reescritura aquí se convierte allí en un recuento de macros por archivo

Dentro del contenedor de compresión MS-OVBA

El formato de compresión de MS-OVBA empaqueta los bytes de fuente en lo que la especificación llama un CompressedContainer: un único byte de firma, que debe ser igual a 0x01, seguido de una secuencia de bloques CompressedChunk, cada uno cubriendo hasta 4096 bytes de datos descomprimidos. Una cabecera de bloque de 16 bits lleva tres campos, una firma de 3 bits que debe ser igual a 3, un campo de tamaño de 12 bits, y un bit CompressedChunkFlag que marca si la carga del bloque es bytes literales o una secuencia comprimida por tokens. Cuando el indicador está activado, la carga es una tirada de grupos de ocho tokens prefijados por byte de indicador, y cada token es o bien un único byte literal o bien un CopyToken: una referencia hacia atrás de desplazamiento/longitud a bytes ya descomprimidos anteriormente en el mismo bloque, con el ancho de bits repartido entre desplazamiento y longitud cambiando según cuán adentro del bloque se encuentre en ese momento el descompresor. Esta parte de MS-OVBA (§2.4.1, Compression and Decompression) es donde una implementación hecha a mano pierde con más frecuencia un día entero por un error de uno en ese cálculo de ancho de bits

Por qué HotXLS escribe bloques en bruto en lugar de emparejar tokens

La vía de escritura de HotXLS esquiva por completo la mitad de emparejamiento de tokens de ese algoritmo. Cuando recomprime un módulo editado, cada bloque sale con el CompressedChunkFlag desactivado, lo que significa que el bloque contiene bytes literales en lugar de tokens de referencia hacia atrás, legal según MS-OVBA, ya que un contenedor comprimido puede consistir enteramente en bloques sin comprimir, y esto elimina precisamente la parte del algoritmo más difícil de acertar a mano: encontrar referencias hacia atrás válidas y empaquetar un par desplazamiento/longitud en un ancho de bits que depende de la posición actual dentro del bloque. La compensación aparece en el tamaño del archivo, no en la corrección: un flujo de módulo reescrito acaba cerca del tamaño de su texto fuente más una cabecera de dos bytes por cada bloque de 4096 bytes, no más pequeño como lo sería un bloque totalmente comprimido por tokens. Cada lector que implemente el lado de descompresión de la especificación, Excel incluido, sigue abriendo el resultado correctamente, porque un bloque en bruto es un CompressedChunk tan válido como uno comprimido por tokens

Qué deja intacto HotXLS cuando reescribe un módulo

La recompresión solo sustituye jamás parte del flujo del módulo. Cada flujo de módulo almacena primero su caché de rendimiento y después su fuente comprimida, y el flujo dir del proyecto registra exactamente dónde cae esa división para cada módulo en una entrada MODULEOFFSET; HotXLS lee ese desplazamiento, conserva cada byte anterior a él exactamente como lo encontró, y reconstruye solo el contenedor comprimido a partir de ese desplazamiento en adelante

El propio texto fuente hace el viaje de ida y vuelta a través de la página de códigos propia del proyecto VBA en lugar de UTF-8, la misma página de códigos heredada con la que Office escribió el proyecto en primer lugar. Una edición de SourceCode que introduce caracteres fuera del repertorio de esa página de códigos se sustituye silenciosamente con caracteres de reemplazo de mejor ajuste cuando HotXLS vuelve a codificar la cadena a bytes, no se rechaza, así que un carácter regional inusual colado en un comentario o en un literal de cadena es el lugar más probable donde notar la pérdida. Las referencias externas y los enlaces a bibliotecas dentro del mismo proyecto siguen una vía de preservación relacionada pero independiente, cubierta en el artículo complementario sobre la preservación de enlaces externos de VBA, y merece la pena leerlo antes de que una pasada de reescritura toque un proyecto que enlaza con otros libros o bibliotecas de tipos

¿Cómo se devuelven las macros reescritas a un libro?

Nada llama al paso de recompresión explícitamente, se ejecuta automáticamente en el momento en que se guarda un libro o un proyecto VBA independiente. TXLSVBAProject.ApplyChanges recorre cada módulo, recomprime aquellos cuyo SourceCode haya cambiado desde el último guardado, y reescribe solo el flujo de ese módulo; tanto el TXLSWorkbook.SaveAs clásico, cuando el destino de guardado conserva el formato original del archivo, como el TXLSXWorkbook.SaveAs de OOXML para un paquete XLSM habilitado para macros llaman a este método internamente antes de escribir nada en disco, y SaveVBAProjectToFile llama al mismo método cuando el destino es un archivo de proyecto VBA independiente en lugar de un libro completo

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;

Los tres destinos comparten por debajo la misma mecánica de SourceCode y ApplyChanges; la única diferencia real entre ellos es qué llamada de guardado acaba disparando la recompresión

Dónde esto todavía se rompe

Dos modos de fallo son lo bastante comunes como para planificarlos antes de que una pasada de reescritura se ejecute contra archivos de producción. Un proyecto VBA firmado digitalmente deja de estar válidamente firmado en el momento en que cambia su fuente, ya que la firma cubre el contenido del proyecto; HotXLS no tiene forma de volver a firmar un proyecto en vuestro nombre, y Excel elimina o marca la firma la próxima vez que se abra el archivo, así que un proyecto de macros firmado necesita un paso de refirmado posterior si esa firma es algo que vuestro flujo de trabajo realmente comprueba. El segundo modo de fallo pertenece a cualquiera tentado de reimplementar este formato de compresión desde cero en lugar de usar una biblioteca que ya lo gestiona: un único bit equivocado en una cabecera de bloque, en el nibble de firma, en el campo de tamaño o en el indicador de compresión, produce un archivo que Excel se niega a abrir, normalmente detrás de una advertencia genérica de corrupción que no da ninguna pista de qué byte estaba mal, precisamente la clase de fallo que la estrategia de escritura en bloques en bruto descrita antes existe para evitar

Nada de esto requiere hacer ingeniería inversa del formato para usarlo. Los desarrolladores Delphi y C++Builder obtienen acceso de lectura y escritura a SourceCode, recompresión conforme a MS-OVBA y los tres destinos de reescritura descritos aquí como parte del componente HotXLS estándar, junto con el resto de su API de libros XLS clásico y OOXML