Premenovanie napevno zadaného odkazu na hárok naprieč tisíckou šablón reportov s makrami vylučuje ručné otváranie každého súboru v editore VBA. HotXLS, natívny komponent Excel pre Delphi a C++Builder, tento prípad rieši tak, že sprístupňuje zdrojový kód modulu VBA ako upraviteľnú vlastnosť SourceCode a rekomprimuje každú úpravu kompresným algoritmom MS-OVBA, ktorý Microsoft definuje špecificky pre úložisko VBA, a zapisuje výsledok späť do úložiska VBA klasického XLS, samostatného súboru projektu VBA, alebo zošita XLSM s makrami. Do tejto cesty nikde nevstupuje žiadna inštancia Excelu, žiadny editor VBA, ani žiadny záznamník makier
Prečo prúd modulu VBA nie je textový súbor
Modul VBA vnútri zošita XLS alebo samostatného súboru projektu VBA nie je zdrojový text sediaci v prúde a čakajúci na prečítanie — je to malý binárny kontajner. Najprv prichádza skompilovaná výkonnostná vyrovnávacia pamäť, bajty, ktoré Office používa na preskočenie opätovnej kompilácie modulu pri načítaní, keď táto vyrovnávacia pamäť ešte zodpovedá hostiteľskej verzii, a za ňou nasleduje samotný zdrojový text, prevedený cez proprietárnu kompresnú schému, ktorú MS-OVBA definuje špecificky pre úložisko VBA. Táto schéma nie je zip, nie je deflate, a nie je nič, čo by natívne produkovali kompresné API Windows, čo je presne dôvod, prečo väčšina knižníc Excelu tretích strán dokáže prečítať zdrojový kód modulu — dekompresia je jednoduchšia polovica problému — no zastaví sa ešte pred jeho zápisom späť, keďže rekompresia je miesto, kde jemne nesprávny bit vyprodukuje súbor, ktorý Excel odmietne otvoriť. Verejné texty o čítacej strane existujú; implementácie zápisovej strany, ktoré skutočne vykonávajú rekompresiu, nie iba rozbaľujú existujúci modul na inšpekciu, sú dosť vzácne na to, aby toto zostalo jedným z najmenej zdokumentovaných kútov formátov súborov Excelu
Čo presne mení vlastnosť SourceCode v HotXLS?
HotXLS reprezentuje každý modul VBA ako objekt TXLSVBAModule s obyčajnou vlastnosťou SourceCode: WideString, a priradenie novej hodnoty jej je presne také jednoduché, ako to vyzerá: modul sa v pamäti označí ako zmenený, a nič sa nedotkne podkladového prúdu OLE, kým sa projekt neuloží. Samotný projekt pochádza z IXLSWorkbook.VBAProject na klasickom engine XLS alebo TXLSXWorkbook.ParsedVBAProject na engine OOXML s makrami, oba vracajú TXLSVBAProject, ktorého moduly sedia za indexátorom Item[] počítaným od 1 a vlastnosťou Count, takže dávková úprava naprieč každým modulom v zošite je iba slučka cez rozsah celých čísel
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;
Táto slučka má aj tvar auditného prechodu. Ešte pred tým, než sa dotkne tisícky šablón, väčšina tímov najprv chce vedieť, koľko z nich v skutočnosti nesie makrá a na čo sa tieto makrá odkazujú, čo je scenár za pracovnou stanicou na audit a konverziu zošitov — ten istý Project.Count, ktorý tu poháňa slučku prepisovania, sa tam stáva súhrnom makier na súbor
Vnútri kompresného kontajnera MS-OVBA
Kompresný formát MS-OVBA balí zdrojové bajty do toho, čo špecifikácia nazýva CompressedContainer: jediný signatúrny bajt, ktorý sa musí rovnať 0x01, nasledovaný postupnosťou blokov CompressedChunk, z ktorých každý pokrýva až 4096 bajtov dekomprimovaných dát. 16-bitová hlavička bloku nesie tri polia — 3-bitovú signatúru, ktorá sa musí rovnať 3, 12-bitové pole veľkosti, a bit CompressedChunkFlag označujúci, či je payload bloku doslovné bajty, alebo sekvencia komprimovaná tokenmi. Keď je príznak nastavený, payload je séria skupín ôsmich tokenov s prefixom bajtu príznakov, a každý token je buď jediný doslovný bajt, alebo CopyToken: spätný odkaz posun/dĺžka na bajty už dekomprimované skôr v tom istom bloku, pričom rozdelenie šírky bitov medzi posun a dĺžku sa mení podľa toho, ako ďaleko vnútri bloku sa dekompresor práve nachádza. Práve táto časť MS-OVBA (§2.4.1, Kompresia a dekompresia) je miesto, kde ručne písaná implementácia najčastejšie stráca deň na chybe o jedna v tomto výpočte šírky bitov
Prečo HotXLS zapisuje surové bloky namiesto porovnávania tokenov
Zápisová cesta HotXLS sa úplne vyhýba polovici tohto algoritmu venovanej porovnávaniu tokenov. Keď rekomprimuje upravený modul, každý blok ide von s vymazaným príznakom CompressedChunkFlag, čo znamená, že blok drží doslovné bajty namiesto spätne odkazujúcich tokenov — legálne podľa MS-OVBA, keďže komprimovaný kontajner smie pozostávať výhradne z nekomprimovaných blokov, a odstraňuje presne tú časť algoritmu, ktorú je najťažšie ručne urobiť správne: nájsť platné spätné odkazy a zbaliť dvojicu posun/dĺžka do šírky bitov, ktorá závisí od aktuálnej pozície vnútri bloku. Kompromis sa prejaví vo veľkosti súboru, nie v správnosti — prepísaný prúd modulu skončí blízko veľkosti svojho zdrojového textu plus dvojbajtová hlavička na 4096-bajtový blok, nie menší, ako by bol plne tokenmi komprimovaný blok. Každá čítačka, ktorá implementuje dekompresnú stranu špecifikácie, Excel v to počítajúc, stále otvorí výsledok správne, pretože surový blok je rovnako platný CompressedChunk ako tokenmi komprimovaný
Čo HotXLS necháva nedotknuté, keď prepisuje modul
Rekompresia nahrádza iba časť prúdu modulu. Každý prúd modulu ukladá najprv svoju výkonnostnú vyrovnávaciu pamäť a potom komprimovaný zdrojový kód, a prúd dir projektu zaznamenáva presne to, kde toto rozdelenie pre každý modul padá, v položke MODULEOFFSET; HotXLS tento posun číta, ponecháva každý bajt pred ním presne tak, ako ho našiel, a znova zostaví iba komprimovaný kontajner od tohto posunu ďalej
Samotný zdrojový text sa prenáša tam a späť cez vlastnú znakovú stránku projektu VBA, nie cez UTF-8 — tú istú staršiu znakovú stránku, s akou Office projekt pôvodne zapísal. Úprava SourceCode, ktorá zavedie znaky mimo repertoáru tejto znakovej stránky, sa pri opätovnom kódovaní reťazca späť na bajty ticho nahradí najbližšie sediacimi náhradnými znakmi, nie odmietne, takže nezvyčajný regionálny znak vložený do komentára alebo reťazcového literálu je najpravdepodobnejšie miesto, kde si stratu všimnete. Externé odkazy a väzby na knižnice vnútri toho istého projektu sledujú súvisiacu, ale samostatnú cestu zachovania, opísanú v sprievodnom článku o zachovaní externých odkazov VBA, a oplatí sa ho prečítať skôr, než sa prechod prepisovania dotkne projektu, ktorý odkazuje von na iné zošity alebo knižnice typov
Ako sa prepísané makrá dostanú späť do zošita?
Nič nevolá krok rekompresie výslovne — beží automaticky vo chvíli, keď sa zošit alebo samostatný projekt VBA uloží. TXLSVBAProject.ApplyChanges prejde každý modul, rekomprimuje tie, ktorých SourceCode sa zmenil od posledného uloženia, a prepíše iba prúd tohto modulu; klasické TXLSWorkbook.SaveAs, keď cieľ uloženia zachová pôvodný formát súboru, a OOXML TXLSXWorkbook.SaveAs pre balík XLSM s makrami obe volajú túto metódu interne ešte predtým, než sa čokoľvek zapíše na disk, a SaveVBAProjectToFile volá tú istú metódu, keď je cieľom odpojený súbor projektu VBA namiesto celého zošita
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;
Všetky tri ciele zdieľajú pod kapotou rovnakú mechaniku SourceCode a ApplyChanges; jediný skutočný rozdiel medzi nimi je to, ktoré volanie uloženia nakoniec spustí rekompresiu
Kde sa to stále láme
Dva režimy zlyhania sú dosť bežné na to, aby sa s nimi počítalo ešte predtým, než prechod prepisovania spustíte voči produkčným súborom. Digitálne podpísaný projekt VBA prestane byť platne podpísaný vo chvíli, keď sa zmení jeho zdrojový kód, keďže podpis pokrýva obsah projektu; HotXLS nemá žiadny spôsob, ako projekt vo vašom mene znova podpísať, a Excel podpis zahodí alebo označí pri najbližšom otvorení súboru, takže podpísaný projekt makier potrebuje downstream krok opätovného podpisovania, ak je tento podpis niečo, čo váš pracovný postup skutočne kontroluje. Druhý režim zlyhania patrí každému, kto je v pokušení znova implementovať tento kompresný formát od nuly namiesto použitia knižnice, ktorá ho už zvláda: jediný nesprávny bit v hlavičke bloku, v nibble signatúry, v poli veľkosti, alebo v komprimovanom príznaku, vyprodukuje súbor, ktorý Excel odmietne otvoriť, zvyčajne za všeobecným varovaním o poškodení, ktoré nedáva žiadnu nápovedu, ktorý bajt bol nesprávny — presne tá trieda chýb, ktorej sa stratégia zápisu surových blokov opísaná vyššie snaží vyhnúť
Nič z toho nevyžaduje na použitie spätné inžinierstvo formátu. Vývojári v Delphi a C++Builder dostávajú prístup na čítanie a zápis SourceCode, rekompresiu v súlade s MS-OVBA, a všetky tri tu opísané ciele zápisu späť ako súčasť štandardného komponentu HotXLS, spolu so zvyškom jeho API pre klasické zošity XLS a OOXML