Teknisk artikel

Omskrivning af VBA-kildekode og genkomprimering af MS-OVBA i Delphi

At omdøbe en hardkodet regnearksreference på tværs af tusind makro-aktiverede rapportskabeloner udelukker at åbne hver fil i VBA-editoren i hånden. HotXLS, den native Delphi- og C++Builder-Excel-komponent, håndterer det tilfælde ved at eksponere et VBA-moduls kildekode som en redigerbar SourceCode-egenskab og genkomprimere hver redigering med MS-OVBA-komprimeringsalgoritmen, Microsoft definerer specifikt til VBA-lagring, og skriver resultatet tilbage til klassisk XLS-VBA-lagring, en selvstændig VBA-projektfil, eller en makro-aktiveret XLSM-projektmappe. Ingen Excel-instans, ingen VBA-editor og ingen makro-optager er involveret noget sted i den vej

Hvorfor en VBA-modulstream ikke er en tekstfil

Et VBA-modul inde i en XLS-projektmappe eller en selvstændig VBA-projektfil er ikke kildetekst, der sidder i en stream og venter på at blive læst — det er en lille binær container. En kompileret ydeevne-cache kommer først, de bytes Office bruger til at springe genkompilering af modulet over ved indlæsning, når cachen stadig matcher værtsversionen, og den faktiske kildetekst følger, kørt gennem et proprietært komprimeringsskema, MS-OVBA definerer specifikt til VBA-lagring. Det skema er ikke zip, ikke deflate, og ikke noget Windows-komprimerings-API'erne producerer nativt, hvilket er præcis, hvorfor de fleste tredjeparts-Excel-biblioteker kan læse et moduls kildekode — dekomprimering er den lettere halvdel af problemet — mens de stopper før at skrive den tilbage, da genkomprimering er, hvor en subtilt forkert bit producerer en fil, Excel nægter at åbne. Offentlige gennemgange af læse-siden findes; skrive-side-implementeringer der rent faktisk udfører genkomprimering, frem for bare at udpakke et eksisterende modul til inspektion, er sparsomme nok til, at dette forbliver et af de mindst dokumenterede hjørner af Excel-filformaterne

Hvad ændrer HotXLS' SourceCode-egenskab rent faktisk?

HotXLS repræsenterer hvert VBA-modul som et TXLSVBAModule-objekt med en almindelig SourceCode: WideString-egenskab, og at tildele den en ny værdi er nøjagtig så simpelt, som det ser ud: modulet markeres beskidt i hukommelsen, og intet rører den underliggende OLE-stream, før projektet gemmes. Selve projektet kommer fra IXLSWorkbook.VBAProject på den klassiske XLS-motor eller TXLSXWorkbook.ParsedVBAProject på den OOXML-makro-aktiverede motor, begge returnerer et TXLSVBAProject, hvis moduler sidder bag en 1-baseret Item[]-indekser og en Count-egenskab, så en batch-redigering på tværs af hvert modul i en projektmappe er bare en løkke over et heltalsinterval

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;

Den løkke er også formen på et audit-gennemløb. Før tusind skabeloner bliver rørt, vil de fleste teams først vide, hvor mange af dem der rent faktisk bærer makroer, og hvad de makroer refererer, hvilket er scenariet bag projektmappe-audit- og konverterings-workbenchen — den samme Project.Count, der driver en omskrivnings-løkke her, bliver en per-fil-makro-optælling der

Inde i MS-OVBA-komprimeringscontaineren

MS-OVBA's komprimeringsformat pakker kildebytes ind i, hvad specifikationen kalder en CompressedContainer: en enkelt signaturbyte, krævet at være lig 0x01, efterfulgt af en sekvens af CompressedChunk-blokke, hver dækker op til 4096 bytes af dekomprimeret data. En 16-bit chunk-header bærer tre felter — en 3-bit-signatur der skal være lig 3, et 12-bit størrelsesfelt, og en CompressedChunkFlag-bit der markerer, om chunkens payload er bogstavelige bytes eller en token-komprimeret sekvens. Når flaget er sat, er payloaden en serie af flag-byte-prefikserede grupper af otte tokens, og hver token er enten en enkelt bogstavelig byte eller en CopyToken: en forskydning/længde-tilbagereference ind i bytes allerede dekomprimeret tidligere i den samme chunk, med bitbredden delt mellem forskydning og længde, der skifter afhængigt af, hvor langt ind i chunken dekomprimeringen aktuelt sidder. Denne del af MS-OVBA (§2.4.1, Compression and Decompression) er, hvor en håndrullet implementering oftest mister en dag på en fejl-med-én i den bitbredde-beregning

Hvorfor HotXLS skriver rå chunks i stedet for at matche tokens

HotXLS' skrivevej omgår token-matchings-halvdelen af den algoritme helt. Når den genkomprimerer et redigeret modul, går hver chunk ud med CompressedChunkFlag ryddet, hvilket betyder at chunken holder bogstavelige bytes frem for tilbagereference-tokens — lovligt under MS-OVBA, da en komprimeret container tillades at bestå udelukkende af ikke-komprimerede chunks, og det fjerner præcis den del af algoritmen, der er sværest at få rigtig i hånden: at finde gyldige tilbagereferencer og pakke et forskydning/længde-par ind i en bitbredde, der afhænger af den nuværende position inde i chunken. Afvejningen viser sig i filstørrelse, ikke korrekthed — en omskrevet modulstream lander tæt på størrelsen af sin kildetekst plus en to-byte-header pr. 4096-byte-blok, ikke mindre på den måde en fuldt token-komprimeret chunk ville være. Hver læser, der implementerer dekomprimerings-siden af specifikationen, Excel inkluderet, åbner stadig resultatet korrekt, fordi en rå chunk er lige så gyldig en CompressedChunk som en token-komprimeret en

Hvad HotXLS lader være urørt, når den omskriver et modul

Genkomprimering erstatter kun nogensinde en del af modulstreamen. Hver modulstream gemmer sin ydeevne-cache først og sin komprimerede kildekode dernæst, og projektets dir-stream registrerer præcis, hvor det skel falder for hvert modul i en MODULEOFFSET-post; HotXLS læser den forskydning, beholder hver byte før den nøjagtig som den fandt dem, og genopbygger kun den komprimerede container fra forskydningen og fremad

Selve kildeteksten går tur-retur gennem VBA-projektets egen kodeside frem for UTF-8 — den samme legacy-kodeside Office skrev projektet med i første omgang. En SourceCode-redigering, der introducerer tegn uden for den kodesides repertoire, bliver i stilhed erstattet med best-fit-erstatningstegn, når HotXLS genkoder strengen tilbage til bytes, ikke afvist, så et usædvanligt regionalt tegn droppet ind i en kommentar eller streng-literal er det mest sandsynlige sted at bemærke tabet. Eksterne referencer og biblioteksbindinger inde i det samme projekt følger en beslægtet, men separat bevaringsvej, dækket i følgeartiklen om bevaring af eksterne VBA-links, og det er værd at læse, før et omskrivningsgennemløb rører et projekt, der linker ud til andre projektmapper eller typebiblioteker

Hvordan får man de omskrevne makroer tilbage ind i en projektmappe?

Intet kalder genkomprimeringstrinnet eksplicit — det kører automatisk, i det øjeblik en projektmappe eller et selvstændigt VBA-projekt gemmes. TXLSVBAProject.ApplyChanges gennemgår hvert modul, genkomprimerer dem hvis SourceCode ændrede sig siden sidste gemning, og omskriver kun det moduls stream; den klassiske TXLSWorkbook.SaveAs, når gemme-målet beholder filens oprindelige format, og OOXML-TXLSXWorkbook.SaveAs til en makro-aktiveret XLSM-pakke kalder begge den internt, før noget skrives til disk, og SaveVBAProjectToFile kalder den samme metode, når målet er en løsrevet VBA-projektfil frem for en fuld projektmappe

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;

Alle tre destinationer deler den samme SourceCode- og ApplyChanges-mekanik nedenunder; den eneste reelle forskel mellem dem er, hvilket gemme-kald der ender med at udløse genkomprimeringen

Hvor dette stadig går i stykker

To fejltilstande er almindelige nok til at planlægge omkring, før et omskrivningsgennemløb kører mod produktionsfiler. Et digitalt signeret VBA-projekt holder op med at være gyldigt signeret, i det øjeblik dets kildekode ændres, da signaturen dækker projektets indhold; HotXLS har ingen måde at gensignere et projekt på dine vegne, og Excel dropper eller flager signaturen, næste gang filen åbnes, så et signeret makroprojekt har brug for et gensigneringstrin nedstrøms, hvis den signatur er noget, ens workflow rent faktisk tjekker. Den anden fejltilstand tilhører alle der er fristet til at genimplementere dette komprimeringsformat fra bunden i stedet for at bruge et bibliotek, der allerede håndterer det: én forkert bit i en chunk-header, i signatur-nibblen, størrelsesfeltet eller det komprimerede flag, producerer en fil, Excel nægter at åbne, sædvanligvis bag en generisk korruptionsadvarsel, der ikke giver noget hint om, hvilken byte der var forkert — netop den klasse af bug, den rå-chunk-skrivestrategi beskrevet tidligere findes for at undgå

Intet af dette kræver reverse-engineering af formatet at bruge. Delphi- og C++Builder-udviklere får SourceCode-læse- og -skriveadgang, MS-OVBA-kompatibel genkomprimering, og alle tre tilbageskrivnings-destinationer beskrevet her som en del af standard-HotXLS-komponenten, sammen med resten af dens klassiske XLS- og OOXML-projektmappe-API