Teknisk artikkel

Å skrive om VBA-kildekode og rekomprimere MS-OVBA i Delphi

Å omdøpe en hardkodet regnearkreferanse på tvers av tusen makroaktiverte rapportmaler utelukker å åpne hver fil i VBA-redigereren for hånd. HotXLS, den native Delphi- og C++Builder-Excel-komponenten, håndterer det tilfellet ved å eksponere en VBA-moduls kildekode som en redigerbar SourceCode-egenskap og rekomprimere hver redigering med MS-OVBA-komprimeringsalgoritmen Microsoft definerer for VBA-lagring, og skriver resultatet tilbake inn i klassisk XLS VBA-lagring, en frittstående VBA-prosjektfil, eller en makroaktivert XLSM-arbeidsbok. Ingen Excel-instans, ingen VBA-redigerer, og ingen makroinnspilling er involvert noe sted i den veien

Hvorfor en VBA-modulstrøm ikke er en tekstfil

En VBA-modul inne i en XLS-arbeidsbok eller en frittstående VBA-prosjektfil er ikke kildetekst som ligger i en strøm og venter på å bli lest — det er en liten binær container. En kompilert ytelsesbuffer kommer først, bytene Office bruker for å hoppe over rekompilering av modulen ved innlasting når bufferen fortsatt matcher vertsversjonen, og selve kildeteksten følger, kjørt gjennom et proprietært komprimeringsskjema MS-OVBA definerer spesifikt for VBA-lagring. Det skjemaet er ikke zip, ikke deflate, og ikke noe Windows-komprimerings-API-ene produserer naturlig, noe som er nøyaktig grunnen til at de fleste tredjeparts-Excel-biblioteker kan lese en moduls kildekode — dekomprimering er den enklere halvparten av problemet — mens de stopper kort av å skrive den tilbake, ettersom rekomprimering er der en subtilt feil bit produserer en fil Excel nekter å åpne. Offentlige gjennomganger av lesesiden finnes; skrivesideimplementasjoner som faktisk utøver rekomprimering, snarere enn bare å pakke ut en eksisterende modul for inspeksjon, er sjeldne nok til at dette forblir et av de minst dokumenterte hjørnene av Excel-filformatene

Hva endrer HotXLS' SourceCode-egenskap egentlig?

HotXLS representerer hver VBA-modul som et TXLSVBAModule-objekt med en ren SourceCode: WideString-egenskap, og å tildele den en ny verdi er nøyaktig så enkelt som det ser ut: modulen merkes skitten i minnet, og ingenting rører den underliggende OLE-strømmen før prosjektet lagres. Selve prosjektet kommer fra IXLSWorkbook.VBAProject på den klassiske XLS-motoren eller TXLSXWorkbook.ParsedVBAProject på den makroaktiverte OOXML-motoren, begge returnerer et TXLSVBAProject hvis moduler sitter bak en 1-basert Item[]-indekser og en Count-egenskap, slik at en batch-redigering på tvers av hver modul i en arbeidsbok bare er en løkke over et heltallsområde

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økken har også formen på en revisjonspassering. Før tusen maler blir rørt, ønsker de fleste team først å vite hvor mange av dem som faktisk bærer makroer og hva de makroene refererer til, noe som er scenarioet bak arbeidsbok-revisjons- og konverterings-arbeidsbenken — den samme Project.Count som driver en omskrivingsløkke her, blir en per-fil-makrotelling der

Inne i MS-OVBA-komprimeringscontaineren

MS-OVBAs komprimeringsformat pakker kildebyte inn i det spesifikasjonen kaller en CompressedContainer: en enkelt signaturbyte, påkrevd å være lik 0x01, etterfulgt av en sekvens av CompressedChunk-blokker, hver dekkende opptil 4096 byte dekomprimert data. En 16-bit chunk-header bærer tre felt — en 3-bit signatur som må være lik 3, et 12-bit størrelsesfelt, og en CompressedChunkFlag-bit som markerer om chunkens payload er bokstavelige byte eller en token-komprimert sekvens. Når flagget er satt, er payloaden en rekke av flagg-byte-prefikserte grupper på åtte tokens, og hver token er enten én enkelt bokstavelig byte eller en CopyToken: en offset/lengde-tilbakereferanse inn i byte allerede dekomprimert tidligere i den samme chunken, med bit-bredden delt mellom offset og lengde skiftende avhengig av hvor langt inn i chunken dekomprimatoren for øyeblikket befinner seg. Denne delen av MS-OVBA (§2.4.1, Compression and Decompression) er der en håndbygget implementasjon oftest mister en dag på en av-med-én-feil i den bit-bredde-beregningen

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

HotXLS' skrivevei omgår token-matchings-halvparten av den algoritmen fullstendig. Når den rekomprimerer en redigert modul, går hver chunk ut med CompressedChunkFlag fjernet, noe som betyr at chunken holder bokstavelige byte snarere enn tilbakereferanse-tokens — lovlig under MS-OVBA, ettersom en komprimert container har lov til å bestå fullstendig av ukomprimerte chunks, og det fjerner nøyaktig den delen av algoritmen som er vanskeligst å få riktig for hånd: å finne gyldige tilbakereferanser og pakke et offset/lengde-par inn i en bit-bredde som avhenger av den gjeldende posisjonen inne i chunken. Avveiningen viser seg i filstørrelse, ikke korrekthet — en omskrevet modulstrøm havner nær størrelsen på kildeteksten pluss en to-byte-header per 4096-byte-blokk, ikke mindre slik en fullstendig token-komprimert chunk ville vært. Hver leser som implementerer dekomprimeringssiden av spesifikasjonen, Excel inkludert, åpner fortsatt resultatet korrekt, fordi en rå chunk er en like gyldig CompressedChunk som en token-komprimert en

Hva HotXLS lar være urørt når den skriver om en modul

Rekomprimering erstatter bare noensinne en del av modulstrømmen. Hver modulstrøm lagrer sin ytelsesbuffer først og sin komprimerte kildekode nummer to, og prosjektets dir-strøm registrerer nøyaktig hvor det skillet faller for hver modul i en MODULEOFFSET-oppføring; HotXLS leser den forskyvningen, beholder hver byte før den nøyaktig slik den fant dem, og gjenoppbygger bare den komprimerte containeren fra forskyvningen og utover

Selve kildeteksten gjør en rundtur gjennom VBA-prosjektets egen kodeside snarere enn UTF-8 — den samme eldre kodesiden Office skrev prosjektet med i utgangspunktet. En SourceCode-redigering som introduserer tegn utenfor den kodesidens repertoar, blir stille erstattet med best-tilpasning-erstatningstegn når HotXLS re-koder strengen tilbake til byte, ikke avvist, så et uvanlig regionalt tegn droppet inn i en kommentar eller strengliteral, er det mest sannsynlige stedet å legge merke til tapet. Eksterne referanser og bibliotekbindinger inne i det samme prosjektet følger en relatert, men separat bevaringsvei, dekket i følgeartikkelen om bevaring av eksterne VBA-lenker, og den er verdt en lesning før en omskrivningspassering rører et prosjekt som lenker ut til andre arbeidsbøker eller typebiblioteker

Hvordan får man de omskrevne makroene tilbake inn i en arbeidsbok?

Ingenting kaller rekomprimeringstrinnet eksplisitt — det kjører automatisk i det øyeblikket en arbeidsbok eller et frittstående VBA-prosjekt lagres. TXLSVBAProject.ApplyChanges går gjennom hver modul, rekomprimerer de hvis SourceCode endret seg siden siste lagring, og skriver om bare den modulens strøm; den klassiske TXLSWorkbook.SaveAs, når lagringsmålet beholder filens opprinnelige format, og OOXML TXLSXWorkbook.SaveAs for en makroaktivert XLSM-pakke kaller begge den internt før noe skrives til disk, og SaveVBAProjectToFile kaller den samme metoden når målet er en frittstående VBA-prosjektfil snarere enn en full arbeidsbok

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 destinasjonene deler den samme SourceCode- og ApplyChanges-mekanikken under; den eneste reelle forskjellen mellom dem er hvilket lagringskall som ender opp med å utløse rekomprimeringen

Hvor dette fortsatt går galt

To feilmodi er vanlige nok til å planlegge for før en omskrivingspassering kjøres mot produksjonsfiler. Et digitalt signert VBA-prosjekt slutter å være gyldig signert i det øyeblikket kildekoden endres, ettersom signaturen dekker prosjektets innhold; HotXLS har ingen måte å re-signere et prosjekt på dine vegne, og Excel dropper eller flagger signaturen neste gang filen åpnes, så et signert makroprosjekt trenger et re-signerings-trinn nedstrøms hvis den signaturen er noe arbeidsflyten din faktisk sjekker. Den andre feilmodusen tilhører alle som fristes til å reimplementere dette komprimeringsformatet fra bunnen av i stedet for å bruke et bibliotek som allerede håndterer det: én enkelt feil bit i en chunk-header, i signatur-nibblen, størrelsesfeltet, eller komprimeringsflagget, produserer en fil Excel nekter å åpne, vanligvis bak en generisk korrupsjonsadvarsel som ikke gir noe hint om hvilken byte som var feil — nøyaktig den klassen av bug rå-chunk-skrivestrategien beskrevet tidligere finnes for å unngå

Ingenting av dette krever å reverse-engineere formatet for å bruke det. Delphi- og C++Builder-utviklere får lese- og skrivetilgang til SourceCode, MS-OVBA-konform rekomprimering, og alle tre tilbakeskrivings-destinasjonene beskrevet her som en del av den standard HotXLS-komponenten, sammen med resten av dens klassiske XLS- og OOXML-arbeidsbok-API