Article technique

Réécrire le code source VBA et recompresser MS-OVBA en Delphi

Renommer une référence de feuille de calcul codée en dur dans mille modèles de rapport activés par macros exclut d'ouvrir chaque fichier dans l'éditeur VBA à la main. HotXLS, le composant Excel natif pour Delphi et C++Builder, traite ce cas en exposant le code source d'un module VBA comme une propriété SourceCode modifiable et en recompressant chaque modification avec l'algorithme de compression MS-OVBA que Microsoft définit pour le stockage VBA, en réécrivant le résultat dans le stockage VBA classique XLS, un fichier de projet VBA autonome, ou un classeur XLSM activé par macros. Aucune instance Excel, aucun éditeur VBA, et aucun enregistreur de macros n'intervient nulle part dans ce chemin

Pourquoi un flux de module VBA n'est pas un fichier texte

Un module VBA à l'intérieur d'un classeur XLS ou d'un fichier de projet VBA autonome n'est pas du texte source assis dans un flux en attente d'être lu — c'est un petit conteneur binaire. Un cache de performance compilé vient en premier, les octets qu'Office utilise pour éviter de recompiler le module au chargement lorsque le cache correspond encore à la version hôte, et le texte source réel suit, passé à travers un schéma de compression propriétaire que MS-OVBA définit spécifiquement pour le stockage VBA. Ce schéma n'est pas du zip, pas du deflate, et rien que les API de compression Windows produisent nativement, ce qui explique précisément pourquoi la plupart des bibliothèques Excel tierces peuvent lire le source d'un module — la décompression est la moitié la plus facile du problème — tout en s'arrêtant avant de le réécrire, car la recompression est là où un bit subtilement erroné produit un fichier qu'Excel refuse d'ouvrir. Des articles publics sur le côté lecture existent ; les implémentations côté écriture qui exercent réellement la recompression, plutôt que de simplement désempaqueter un module existant pour inspection, sont assez rares pour que cela reste l'un des coins les moins documentés des formats de fichiers Excel

Que change réellement la propriété SourceCode de HotXLS ?

HotXLS représente chaque module VBA comme un objet TXLSVBAModule avec une simple propriété SourceCode: WideString, et lui assigner une nouvelle valeur est exactement aussi simple qu'il y paraît : le module est marqué sale en mémoire, et rien ne touche le flux OLE sous-jacent jusqu'à ce que le projet soit enregistré. Le projet lui-même provient de IXLSWorkbook.VBAProject sur le moteur XLS classique ou de TXLSXWorkbook.ParsedVBAProject sur le moteur OOXML activé par macros, tous deux renvoyant un TXLSVBAProject dont les modules se trouvent derrière un indexeur Item[] basé sur 1 et une propriété Count, si bien qu'une modification par lot sur chaque module d'un classeur n'est qu'une boucle sur une plage d'entiers

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;

Cette boucle est aussi la forme d'une passe d'audit. Avant de toucher à mille modèles, la plupart des équipes veulent d'abord savoir combien d'entre eux portent réellement des macros et à quoi ces macros font référence, ce qui est le scénario derrière l'atelier d'audit et de conversion de classeurs — le même Project.Count qui pilote une boucle de réécriture ici devient un décompte de macros par fichier là-bas

À l'intérieur du conteneur de compression MS-OVBA

Le format de compression de MS-OVBA emballe les octets source dans ce que la spécification appelle un CompressedContainer : un unique octet de signature, devant être égal à 0x01, suivi d'une séquence de blocs CompressedChunk, chacun couvrant jusqu'à 4096 octets de données décompressées. Un en-tête de bloc de 16 bits porte trois champs — une signature de 3 bits devant valoir 3, un champ de taille de 12 bits, et un bit CompressedChunkFlag marquant si la charge utile du bloc est constituée d'octets littéraux ou d'une séquence compressée par jetons. Lorsque le drapeau est défini, la charge utile est une suite de groupes de huit jetons préfixés par un octet drapeau, et chaque jeton est soit un unique octet littéral, soit un CopyToken : une référence arrière décalage/longueur vers des octets déjà décompressés plus tôt dans le même bloc, avec la largeur de bits répartie entre décalage et longueur variant selon la profondeur atteinte par le décompresseur dans le bloc. C'est cette partie de MS-OVBA (§2.4.1, Compression and Decompression) où une implémentation écrite à la main perd le plus souvent une journée sur une erreur de décalage d'un dans ce calcul de largeur de bits

Pourquoi HotXLS écrit des blocs bruts plutôt que de faire correspondre des jetons

Le chemin d'écriture de HotXLS contourne entièrement la moitié correspondance-de-jetons de cet algorithme. Lorsqu'il recompresse un module modifié, chaque bloc sort avec le CompressedChunkFlag effacé, ce qui signifie que le bloc contient des octets littéraux plutôt que des jetons de référence arrière — légal selon MS-OVBA, puisqu'un conteneur compressé est autorisé à être constitué entièrement de blocs non compressés, et cela retire précisément la partie de l'algorithme la plus difficile à faire correctement à la main : trouver des références arrière valides et empaqueter une paire décalage/longueur dans une largeur de bits qui dépend de la position actuelle à l'intérieur du bloc. Le compromis se manifeste dans la taille du fichier, pas dans la correction — un flux de module réécrit se rapproche de la taille de son texte source plus un en-tête de deux octets par bloc de 4096 octets, pas plus petit comme le serait un bloc entièrement compressé par jetons. Chaque lecteur qui implémente le côté décompression de la spécification, Excel inclus, ouvre toujours le résultat correctement, car un bloc brut est un CompressedChunk tout aussi valide qu'un bloc compressé par jetons

Ce que HotXLS laisse intact lorsqu'il réécrit un module

La recompression ne remplace jamais qu'une partie du flux de module. Chaque flux de module stocke d'abord son cache de performance et son source compressé ensuite, et le flux dir du projet enregistre exactement où tombe cette scission pour chaque module dans une entrée MODULEOFFSET ; HotXLS lit ce décalage, conserve chaque octet avant celui-ci exactement tel qu'il l'a trouvé, et ne reconstruit que le conteneur compressé à partir du décalage vers l'avant

Le texte source lui-même fait l'aller-retour via la propre page de code du projet VBA plutôt que via UTF-8 — la même page de code héritée avec laquelle Office a écrit le projet en premier lieu. Une modification de SourceCode qui introduit des caractères en dehors du répertoire de cette page de code se voit silencieusement substituée par des caractères de remplacement au mieux lorsque HotXLS ré-encode la chaîne en octets, pas rejetée, si bien qu'un caractère régional inhabituel déposé dans un commentaire ou un littéral de chaîne est l'endroit le plus probable pour remarquer la perte. Les références externes et les liaisons de bibliothèque à l'intérieur du même projet suivent un chemin de préservation apparenté mais distinct, couvert dans l'article compagnon sur la préservation des liens externes VBA, et cela mérite une lecture avant qu'une passe de réécriture ne touche un projet lié à d'autres classeurs ou bibliothèques de types

Comment récupérer les macros réécrites dans un classeur ?

Rien n'appelle explicitement l'étape de recompression — elle s'exécute automatiquement au moment où un classeur ou un projet VBA autonome est enregistré. TXLSVBAProject.ApplyChanges parcourt chaque module, recompresse ceux dont SourceCode a changé depuis le dernier enregistrement, et réécrit uniquement le flux de ce module ; le classique TXLSWorkbook.SaveAs, lorsque la cible d'enregistrement conserve le format d'origine du fichier, et l'OOXML TXLSXWorkbook.SaveAs pour un package XLSM activé par macros appellent tous deux cette méthode en interne avant que quoi que ce soit ne soit écrit sur disque, et SaveVBAProjectToFile appelle la même méthode lorsque la cible est un fichier de projet VBA détaché plutôt qu'un classeur complet

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;

Les trois destinations partagent la même mécanique SourceCode et ApplyChanges en dessous ; la seule véritable différence entre elles est de savoir quel appel d'enregistrement finit par déclencher la recompression

Où cela casse encore

Deux modes d'échec sont assez courants pour qu'on les planifie avant qu'une passe de réécriture ne s'exécute contre des fichiers de production. Un projet VBA signé numériquement cesse d'être validement signé au moment où son source change, puisque la signature couvre le contenu du projet ; HotXLS n'a aucun moyen de resigner un projet pour votre compte, et Excel abandonne ou signale la signature la prochaine fois que le fichier s'ouvre, si bien qu'un projet de macros signé a besoin d'une étape de re-signature en aval si cette signature est quelque chose que votre flux de travail vérifie réellement. Le second mode d'échec appartient à quiconque tenté de réimplémenter ce format de compression à partir de zéro plutôt que d'utiliser une bibliothèque qui le gère déjà : un seul bit erroné dans un en-tête de bloc, dans le quartet de signature, le champ de taille, ou le drapeau de compression, produit un fichier qu'Excel refuse d'ouvrir, généralement derrière un avertissement de corruption générique qui ne donne aucune indication sur quel octet était erroné — précisément la classe de bogue que la stratégie d'écriture par blocs bruts décrite plus haut existe pour éviter

Rien de tout cela ne nécessite de rétro-ingénierer le format pour l'utiliser. Les développeurs Delphi et C++Builder obtiennent un accès en lecture et en écriture à SourceCode, une recompression conforme MS-OVBA, et les trois destinations de réécriture décrites ici dans le cadre du composant HotXLS standard, aux côtés du reste de son API de classeur XLS classique et OOXML