Articolo tecnico

Riscrivere il Sorgente VBA e Ricomprimere MS-OVBA in Delphi

Rinominare un riferimento a foglio di lavoro codificato in modo fisso in mille modelli di report abilitati per macro esclude di per sé l'apertura di ogni file nell'editor VBA a mano. HotXLS, il componente Excel nativo per Delphi e C++Builder, gestisce questo caso esponendo il sorgente di un modulo VBA come una proprietà SourceCode modificabile e ricomprimendo ogni modifica con l'algoritmo di compressione MS-OVBA che Microsoft definisce per lo storage VBA, riscrivendo il risultato nello storage VBA di un XLS classico, in un file di progetto VBA autonomo, o in una cartella di lavoro XLSM abilitata per macro. Nessuna istanza di Excel, nessun editor VBA e nessun registratore di macro è coinvolto in nessun punto di quel percorso

Perché uno stream di modulo VBA non è un file di testo

Un modulo VBA dentro una cartella di lavoro XLS o un file di progetto VBA autonomo non è testo sorgente seduto in uno stream in attesa di essere letto — è un piccolo contenitore binario. Prima viene una cache di performance compilata, i byte che Office usa per saltare la ricompilazione del modulo al caricamento quando la cache corrisponde ancora alla versione host, e il testo sorgente vero e proprio segue, passato attraverso uno schema di compressione proprietario che MS-OVBA definisce specificamente per lo storage VBA. Quello schema non è zip, non è deflate, e non è nulla che le API di compressione di Windows producano nativamente, ed è esattamente per questo che la maggior parte delle librerie Excel di terze parti riesce a leggere il sorgente di un modulo — la decompressione è la metà più facile del problema — mentre si ferma prima di riscriverlo, poiché la ricompressione è dove un bit sbagliato in modo sottile produce un file che Excel rifiuta di aprire. Esistono trattazioni pubbliche del lato di lettura; implementazioni del lato di scrittura che esercitino realmente la ricompressione, anziché limitarsi a scompattare un modulo esistente per ispezione, sono abbastanza scarse da far sì che questo rimanga uno degli angoli meno documentati dei formati file di Excel

Cosa cambia realmente la proprietà SourceCode di HotXLS?

HotXLS rappresenta ogni modulo VBA come un oggetto TXLSVBAModule con una semplice proprietà SourceCode: WideString, e assegnarle un nuovo valore è esattamente semplice come sembra: il modulo viene marcato come modificato in memoria, e nulla tocca lo stream OLE sottostante finché il progetto non viene salvato. Il progetto stesso proviene da IXLSWorkbook.VBAProject sul motore XLS classico o da TXLSXWorkbook.ParsedVBAProject sul motore OOXML abilitato per macro, entrambi restituiscono un TXLSVBAProject i cui moduli risiedono dietro un indicizzatore Item[] a base 1 e una proprietà Count, quindi una modifica in blocco su ogni modulo di una cartella di lavoro è semplicemente un ciclo su un intervallo intero

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;

Quel ciclo è anche la forma di un passaggio di audit. Prima che mille modelli vengano toccati, la maggior parte dei team vuole prima sapere quanti di essi portano realmente macro e a cosa quelle macro fanno riferimento, che è lo scenario dietro il banco di lavoro di audit e conversione delle cartelle di lavoro — lo stesso Project.Count che guida un ciclo di riscrittura qui diventa lì un conteggio di macro per file

Dentro il contenitore di compressione MS-OVBA

Il formato di compressione di MS-OVBA impacchetta i byte sorgente in ciò che la specifica chiama CompressedContainer: un singolo byte di firma, che deve essere uguale a 0x01, seguito da una sequenza di blocchi CompressedChunk, ciascuno che copre fino a 4096 byte di dato decompresso. Un'intestazione di chunk a 16 bit porta tre campi — una firma a 3 bit che deve essere uguale a 3, un campo dimensione a 12 bit, e un bit CompressedChunkFlag che segna se il payload del chunk sia byte letterali o una sequenza compressa a token. Quando il flag è impostato, il payload è una serie di gruppi di otto token preceduti da un byte flag, e ogni token è o un singolo byte letterale o un CopyToken: un riferimento all'indietro offset/lunghezza a byte già decompressi in precedenza nello stesso chunk, con la larghezza in bit divisa tra offset e lunghezza che varia a seconda di quanto in profondità nel chunk il decompressore si trovi attualmente. Questa parte di MS-OVBA (§2.4.1, Compression and Decompression) è dove un'implementazione scritta a mano più spesso perde una giornata su un errore di uno nel calcolo di quella larghezza in bit

Perché HotXLS scrive chunk grezzi invece di far corrispondere token

Il percorso di scrittura di HotXLS evita del tutto la metà di corrispondenza dei token di quell'algoritmo. Quando ricomprime un modulo modificato, ogni chunk esce con il CompressedChunkFlag azzerato, il che significa che il chunk contiene byte letterali invece di token di riferimento all'indietro — legale secondo MS-OVBA, poiché a un contenitore compresso è permesso consistere interamente di chunk non compressi, e questo rimuove esattamente la parte dell'algoritmo più difficile da ottenere correttamente a mano: trovare riferimenti all'indietro validi e impacchettare una coppia offset/lunghezza in una larghezza in bit che dipende dalla posizione attuale dentro il chunk. Il compromesso si manifesta nella dimensione del file, non nella correttezza — uno stream di modulo riscritto finisce vicino alla dimensione del proprio testo sorgente più un'intestazione di due byte per blocco da 4096 byte, non più piccolo come sarebbe un chunk completamente compresso a token. Ogni lettore che implementi il lato di decompressione della specifica, Excel incluso, apre comunque correttamente il risultato, perché un chunk grezzo è un CompressedChunk valido quanto uno compresso a token

Cosa lascia intatto HotXLS quando riscrive un modulo

La ricompressione sostituisce sempre e solo parte dello stream del modulo. Ogni stream di modulo memorizza prima la propria cache di performance e poi il proprio sorgente compresso, e lo stream dir del progetto registra esattamente dove cade quella separazione per ciascun modulo in una voce MODULEOFFSET; HotXLS legge quell'offset, mantiene esattamente invariato ogni byte precedente così come lo ha trovato, e ricostruisce solo il contenitore compresso a partire da quell'offset in poi

Il testo sorgente stesso viaggia andata e ritorno attraverso la code page propria del progetto VBA piuttosto che UTF-8 — la stessa code page legacy con cui Office ha scritto il progetto in origine. Una modifica di SourceCode che introduce caratteri al di fuori del repertorio di quella code page viene silenziosamente sostituita con caratteri di rimpiazzo best-fit quando HotXLS ricodifica la stringa in byte, non rifiutata, quindi un carattere regionale insolito inserito in un commento o in un letterale stringa è il punto più probabile in cui notare la perdita. I riferimenti esterni e i binding a librerie dentro lo stesso progetto seguono un percorso di preservazione correlato ma separato, trattato nell'articolo di approfondimento sulla preservazione dei link esterni VBA, e vale la pena leggerlo prima che un passaggio di riscrittura tocchi un progetto che collega ad altre cartelle di lavoro o librerie di tipi

Come si riportano le macro riscritte in una cartella di lavoro?

Nulla chiama esplicitamente il passaggio di ricompressione — viene eseguito automaticamente nel momento in cui una cartella di lavoro o un progetto VBA autonomo viene salvato. TXLSVBAProject.ApplyChanges percorre ogni modulo, ricomprime quelli il cui SourceCode è cambiato dall'ultimo salvataggio, e riscrive solo lo stream di quel modulo; sia il classico TXLSWorkbook.SaveAs, quando la destinazione di salvataggio mantiene il formato originale del file, sia l'OOXML TXLSXWorkbook.SaveAs per un pacchetto XLSM abilitato per macro lo chiamano internamente prima che qualcosa venga scritto su disco, e SaveVBAProjectToFile chiama lo stesso metodo quando la destinazione è un file di progetto VBA distaccato piuttosto che una cartella di lavoro 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;

Tutte e tre le destinazioni condividono sotto il cofano gli stessi meccanismi di SourceCode e ApplyChanges; l'unica vera differenza tra loro è quale chiamata di salvataggio finisca per scatenare la ricompressione

Dove questo si rompe ancora

Due modalità di fallimento sono abbastanza comuni da pianificare in anticipo prima che un passaggio di riscrittura venga eseguito contro file di produzione. Un progetto VBA firmato digitalmente smette di essere validamente firmato nel momento in cui il suo sorgente cambia, poiché la firma copre il contenuto del progetto; HotXLS non ha modo di rifirmare un progetto per tuo conto, ed Excel elimina o segnala la firma la volta successiva che il file viene aperto, quindi un progetto macro firmato ha bisogno di un passaggio di rifirma a valle se quella firma è qualcosa che il tuo flusso di lavoro effettivamente controlla. La seconda modalità di fallimento appartiene a chiunque sia tentato di reimplementare questo formato di compressione da zero invece di usare una libreria che già lo gestisce: un singolo bit sbagliato in un'intestazione di chunk, nel nibble di firma, nel campo dimensione, o nel flag compresso, produce un file che Excel rifiuta di aprire, di solito dietro un avviso generico di corruzione che non dà alcun indizio su quale byte fosse sbagliato — esattamente la classe di bug che la strategia di scrittura a chunk grezzi descritta sopra esiste per evitare

Nulla di tutto ciò richiede reverse engineering del formato per essere usato. Gli sviluppatori Delphi e C++Builder ottengono accesso in lettura e scrittura a SourceCode, ricompressione conforme a MS-OVBA, e tutte e tre le destinazioni di riscrittura descritte qui come parte del componente HotXLS standard, insieme al resto della sua API per cartelle di lavoro XLS classiche e OOXML