Műszaki cikk

VBA forráskód átírása és MS-OVBA újratömörítés Delphiben

Egy kemény kódolt munkalap-hivatkozás átnevezése ezer makróengedélyezett jelentéssablon között kizárja, hogy minden fájlt kézzel megnyissunk a VBA-szerkesztőben. A HotXLS, a natív Delphi és C++Builder Excel-komponens, ezt az esetet úgy kezeli, hogy egy VBA-modul forrását szerkeszthető SourceCode tulajdonságként teszi elérhetővé, és minden szerkesztést újratömörít az MS-OVBA tömörítési algoritmussal, amelyet a Microsoft definiál a VBA-tároláshoz, majd az eredményt visszaírja klasszikus XLS VBA-tárolásba, egy önálló VBA-projektfájlba, vagy egy makróengedélyezett XLSM-munkafüzetbe. Egyetlen Excel-példány, VBA-szerkesztő, vagy makrórögzítő sem érintett sehol ebben az útvonalban

Miért nem szövegfájl egy VBA-modulfolyam

Egy VBA-modul egy XLS-munkafüzeten vagy egy önálló VBA-projektfájlon belül nem forrásszöveg, amely egy streamben ül, olvasásra várva — egy kis bináris konténer. Egy fordított teljesítménygyorsítótár jön először, azok a bájtok, amiket az Office arra használ, hogy kihagyja a modul újrafordítását betöltéskor, amikor a gyorsítótár még illeszkedik a gazdaverzióra, és a tényleges forrásszöveg következik, egy proprietáris tömörítési sémán átfuttatva, amit az MS-OVBA kifejezetten a VBA-tároláshoz definiál. Az a séma nem zip, nem deflate, és nem semmi, amit a Windows tömörítési API-k natívan előállítanak, és pontosan ezért tudja a legtöbb harmadik féltől származó Excel-könyvtár olvasni egy modul forrását — a dekompresszió a probléma könnyebb fele —, miközben nem jut el a visszaíráshoz, mivel az újratömörítés az, ahol egy finoman rossz bit egy olyan fájlt eredményez, amelyet az Excel megtagad megnyitni. Léteznek nyilvános írások az olvasási oldalról; olyan írási oldali megvalósítások, amelyek ténylegesen gyakorolják az újratömörítést, nem pedig csak kicsomagolják egy meglévő modult vizsgálatra, elég ritkák ahhoz, hogy ez maradjon az Excel-fájlformátumok egyik legkevésbé dokumentált sarka

Mit változtat meg ténylegesen a HotXLS SourceCode tulajdonsága?

A HotXLS minden VBA-modult egy TXLSVBAModule objektumként reprezentál egy egyszerű SourceCode: WideString tulajdonsággal, és egy új érték hozzárendelése pontosan olyan egyszerű, amilyennek látszik: a modul memóriában piszkosnak jelölődik meg, és semmi nem nyúl az alapul szolgáló OLE-streamhez, amíg a projekt el nem mentődik. Maga a projekt a IXLSWorkbook.VBAProject-ből származik a klasszikus XLS-motoron, vagy a TXLSXWorkbook.ParsedVBAProject-ből az OOXML makróengedélyezett motoron, mindkettő egy TXLSVBAProject-et ad vissza, amelynek moduljai egy 1-alapú Item[] indexelő és egy Count tulajdonság mögött ülnek, így egy kötegelt szerkesztés egy munkafüzet minden modulján keresztül csupán egy ciklus egy egész tartományon

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;

Ez a ciklus egy audit-átfutás alakja is. Mielőtt ezer sablonhoz hozzányúlnának, a legtöbb csapat először azt akarja tudni, hányan hordoznak közülük ténylegesen makrókat, és mire hivatkoznak azok a makrók, ez a munkafüzet-audit és -konverziós munkapad mögötti forgatókönyv — ugyanaz a Project.Count, amely itt egy átíró ciklust vezérel, ott fájlonkénti makró-számlálássá válik

Az MS-OVBA tömörítési konténer belseje

Az MS-OVBA tömörítési formátuma egy CompressedContainer-be csomagolja a forrásbájtokat, amint a specifikáció nevezi: egyetlen aláírásbájt, amelynek 0x01-nek kell lennie, amit CompressedChunk blokkok sorozata követ, mindegyik legfeljebb 4096 bájt dekompresszált adatot fed le. Egy 16 bites chunk-fejléc három mezőt hordoz — egy 3 bites aláírást, amelynek 3-nak kell lennie, egy 12 bites méretmezőt, és egy CompressedChunkFlag bitet, amely megjelöli, hogy a chunk payloadja szó szerinti bájtok-e, vagy egy token-tömörített sorozat. Amikor a jelző be van állítva, a payload egy sorozat flag-bájttal előtagolt csoportból, nyolc tokenenként, és minden token vagy egyetlen szó szerinti bájt, vagy egy CopyToken: egy eltolás/hossz visszahivatkozás olyan bájtokra, amelyek már korábban dekompresszálódtak ugyanabban a chunkban, az eltolás és hossz közötti bitszélesség-felosztás attól függően vált, milyen mélyen ül jelenleg a dekompresszor a chunkban. Ez a része az MS-OVBA-nak (§2.4.1, Compression and Decompression) az, ahol egy kézzel hengerelt megvalósítás leggyakrabban veszít el egy napot egy off-by-one hibán abban a bitszélesség-számításban

Miért ír a HotXLS nyers chunkokat token-illesztés helyett

A HotXLS írási útvonala teljesen megkerüli az algoritmus token-illesztő felét. Amikor egy szerkesztett modult újratömörít, minden chunk törölt CompressedChunkFlag-gel megy ki, ami azt jelenti, hogy a chunk szó szerinti bájtokat tartalmaz, nem visszahivatkozás-tokeneket — legális az MS-OVBA alatt, mivel egy tömörített konténer teljes egészében állhat tömörítetlen chunkokból, és pontosan azt a részét távolítja el az algoritmusnak, amit a legnehezebb kézzel jól megcsinálni: érvényes visszahivatkozások megtalálását, és egy eltolás/hossz pár csomagolását egy olyan bitszélességbe, amely a chunk aktuális pozíciójától függ. A kompromisszum a fájlméretben mutatkozik meg, nem a helyességben — egy újraírt modulfolyam a forrásszövege méretéhez plusz egy kétbájtos fejléc/4096 bájtos blokkhoz közel landol, nem kisebb, ahogyan egy teljesen token-tömörített chunk lenne. Minden olvasó, amely megvalósítja a specifikáció dekompressziós oldalát, az Excelt is beleértve, még mindig helyesen nyitja meg az eredményt, mert egy nyers chunk pontosan olyan érvényes CompressedChunk, mint egy token-tömörített

Mit hagy érintetlenül a HotXLS, amikor átír egy modult

Az újratömörítés csak a modulfolyam egy részét cseréli le. Minden modulfolyam először a teljesítménygyorsítótárát tárolja, aztán a tömörített forrását, és a projekt dir streamje pontosan rögzíti, hol esik ez a felosztás minden modulnál egy MODULEOFFSET bejegyzésben; a HotXLS beolvassa ezt az eltolást, minden bájtot megtart előtte pontosan úgy, ahogyan megtalálta, és csak a tömörített konténert építi újra az eltolástól kezdve

Maga a forrásszöveg a VBA-projekt saját kódlapján keresztül megy oda-vissza, nem UTF-8-on — ugyanazon az örökölt kódlapon, amellyel az Office eredetileg megírta a projektet. Egy SourceCode szerkesztés, amely az adott kódlap repertoárján kívüli karaktereket vezet be, csendben legjobb-illeszkedésű helyettesítő karakterekkel kerül lecserélésre, amikor a HotXLS visszakódolja a sztringet bájtokká, nem elutasítva, így egy szokatlan regionális karakter, amelyet egy megjegyzésbe vagy sztring-literálba dobtak, a legvalószínűbb hely, ahol a veszteséget észrevesszük. A külső hivatkozások és könyvtárkötések ugyanazon a projekten belül egy rokon, de külön megőrzési útvonalat követnek, amelyet a VBA külső hivatkozások megőrzéséről szóló kísérőcikk tárgyal, és érdemes elolvasni, mielőtt egy átíró átfutás hozzáér egy olyan projekthez, amely más munkafüzetekre vagy típuskönyvtárakra hivatkozik ki

Hogyan kerülnek vissza az átírt makrók egy munkafüzetbe?

Semmi nem hívja meg explicit módon az újratömörítési lépést — az automatikusan lefut abban a pillanatban, amikor egy munkafüzet vagy egy önálló VBA-projekt mentésre kerül. A TXLSVBAProject.ApplyChanges bejár minden modult, újratömöríti azokat, amelyek SourceCode-ja megváltozott a legutóbbi mentés óta, és csak annak a modulnak a folyamát írja át; a klasszikus TXLSWorkbook.SaveAs, amikor a mentés célja megtartja a fájl eredeti formátumát, és az OOXML TXLSXWorkbook.SaveAs egy makróengedélyezett XLSM-csomaghoz, mindkettő belsőleg meghívja azt, mielőtt bármi is a lemezre íródna, és a SaveVBAProjectToFile ugyanazt a metódust hívja, amikor a cél egy leválasztott VBA-projektfájl, nem egy teljes munkafüzet

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;

Mindhárom célhely ugyanazon a SourceCode és ApplyChanges gépezeten osztozik alul; az egyetlen valódi különbség köztük az, melyik mentési hívás váltja ki végül az újratömörítést

Hol törik el ez még mindig

Két hibamód elég gyakori ahhoz, hogy tervezzünk vele, mielőtt egy átíró átfutás termelési fájlok ellen fut. Egy digitálisan aláírt VBA-projekt megszűnik érvényesen aláírtnak lenni abban a pillanatban, amikor a forrása megváltozik, mivel az aláírás a projekt tartalmát fedi le; a HotXLS-nek nincs módja a nevedben újraaláírni egy projektet, és az Excel eldobja vagy megjelöli az aláírást a fájl következő megnyitásakor, így egy aláírt makróprojektnek egy downstream újraaláírási lépésre van szüksége, ha az az aláírás olyasmi, amit a munkafolyamatod ténylegesen ellenőriz. A második hibamód azokhoz tartozik, akiket megkísért, hogy nulláról újravalósítsák ezt a tömörítési formátumot ahelyett, hogy egy olyan könyvtárat használnának, amely már kezeli azt: egyetlen rossz bit egy chunk-fejlécben, az aláírás-nibble-ben, a mezőméretben, vagy a tömörített jelzőben, egy olyan fájlt eredményez, amelyet az Excel megtagad megnyitni, általában egy generikus sérülésfigyelmeztetés mögött, amely semmi nyomot nem ad arra, melyik bájt volt rossz — pontosan az a hibaosztály, aminek elkerülésére a korábban leírt nyers-chunk írási stratégia létezik

Ehhez semmi nem igényli a formátum visszafejtését a használatához. A Delphi- és C++Builder-fejlesztők SourceCode olvasási és írási hozzáférést, MS-OVBA-kompatibilis újratömörítést, és mind a három itt leírt visszaírási célhelyet megkapják a szabványos HotXLS Komponens részeként, a klasszikus XLS- és OOXML-munkafüzet-API többi részével együtt