Tehnički članak

Prepisivanje VBA izvora i rekompresija MS-OVBA u Delphiju

Preimenovanje tvrdokodno zapisane reference na radni list kroz tisuću predložaka izvještaja s makronaredbama isključuje ručno otvaranje svake datoteke u VBA uređivaču. HotXLS, izvorna Excel komponenta za Delphi i C++Builder, rješava taj slučaj tako što izvor VBA modula izlaže kao svojstvo SourceCode koje se može uređivati i svaku izmjenu ponovno komprimira algoritmom MS-OVBA koji Microsoft definira za pohranu VBA sadržaja, a rezultat zapisuje natrag u klasičnu XLS VBA pohranu, zasebnu datoteku VBA projekta ili radnu knjigu XLSM s makronaredbama. Na tom putu nema instance Excela, VBA uređivača ni snimača makronaredbi

Zašto tok VBA modula nije tekstualna datoteka

VBA modul unutar XLS radne knjige ili zasebne datoteke VBA projekta nije izvorni tekst koji čeka u toku da ga netko pročita — riječ je o malom binarnom spremniku. Najprije dolazi kompajlirana predmemorija performansi, odnosno bajtovi koje Office koristi da pri učitavanju preskoči ponovnu kompilaciju modula kada se predmemorija i dalje podudara s verzijom glavnog programa, a zatim slijedi stvarni izvorni tekst obrađen vlasničkom shemom kompresije koju MS-OVBA definira posebno za pohranu VBA sadržaja. Ta shema nije zip, nije deflate i nije nešto što Windowsovi API-ji za kompresiju izvorno proizvode, zbog čega većina Excel biblioteka trećih strana može pročitati izvor modula — dekompresija je lakša polovica problema — ali staje prije ponovnog zapisivanja jer upravo rekompresija može zbog jedne neznatno pogrešne vrijednosti proizvesti datoteku koju Excel odbija otvoriti. Javna objašnjenja postupka čitanja postoje, ali implementacije zapisivanja koje doista provode rekompresiju, a ne samo raspakiravaju postojeći modul radi pregleda, toliko su rijetke da je ovo i dalje jedan od najmanje dokumentiranih kutaka Excelovih formata datoteka

Što svojstvo SourceCode u HotXLS-u zapravo mijenja

HotXLS svaki VBA modul predstavlja objektom TXLSVBAModule s običnim svojstvom SourceCode: WideString, a dodjela nove vrijednosti doista je jednostavna: modul se u memoriji označava kao izmijenjen i ništa ne dira temeljni OLE tok dok se projekt ne spremi. Sam projekt dolazi iz IXLSWorkbook.VBAProject u klasičnom XLS mehanizmu ili iz TXLSXWorkbook.ParsedVBAProject u OOXML mehanizmu za radne knjige s makronaredbama, a oba vraćaju TXLSVBAProject čiji se moduli dohvaćaju putem indeksatora Item[] s indeksiranjem od 1 i svojstva Count, pa je skupna izmjena svih modula u radnoj knjizi samo petlja kroz cijeli raspon cijelih brojeva

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;

Ista je petlja i oblik revizijskog prolaza. Prije obrade tisuću predložaka većina timova najprije želi znati koliko ih doista sadrži makronaredbe i na što se te makronaredbe pozivaju, što je upravo slučaj za radni stol za reviziju i pretvorbu radnih knjiga — isti Project.Count koji ovdje pokreće petlju za prepisivanje ondje postaje broj makronaredbi po datoteci

Unutrašnjost spremnika kompresije MS-OVBA

Format kompresije MS-OVBA pakira izvorne bajtove u ono što specifikacija naziva CompressedContainer: jedan potpisni bajt koji mora biti jednak 0x01, nakon kojeg slijedi niz blokova CompressedChunk, pri čemu svaki obuhvaća najviše 4096 bajtova dekomprimiranih podataka. Zaglavlje bloka od 16 bitova nosi tri polja — potpis od 3 bita koji mora biti jednak 3, polje veličine od 12 bitova i bit CompressedChunkFlag koji označava jesu li podaci bloka doslovni bajtovi ili slijed komprimiran tokenima. Kada je zastavica postavljena, podaci su niz grupa od osam tokena kojima prethodi zastavni bajt, a svaki token je ili jedan doslovni bajt ili CopyToken, odnosno povratna referenca pomaka i duljine na bajtove već dekomprimirane ranije u istom bloku, pri čemu se širina bitova podijeljenih između pomaka i duljine mijenja ovisno o tome koliko je dekompresor trenutačno odmaknut unutar bloka. Upravo se u tom dijelu MS-OVBA specifikacije (§2.4.1, Kompresija i dekompresija) ručno pisana implementacija najčešće spotakne o pogrešku za jedan u izračunu širine bitova

Zašto HotXLS zapisuje sirove blokove umjesto podudaranja tokena

HotXLS u potpunosti zaobilazi polovicu algoritma koja se bavi podudaranjem tokena. Pri rekompresiji izmijenjenog modula svaki blok izlazi s poništenim CompressedChunkFlagom, što znači da sadrži doslovne bajtove umjesto tokena s povratnim referencama — to je dopušteno u MS-OVBA jer se komprimirani spremnik smije u cijelosti sastojati od nekomprimiranih blokova, a pritom se uklanja upravo dio algoritma koji je najteže ručno ispravno izvesti: pronalaženje valjanih povratnih referenci i pakiranje para pomaka i duljine u širinu bitova koja ovisi o trenutačnom položaju unutar bloka. Posljedica se vidi u veličini datoteke, a ne u ispravnosti — tok ponovno zapisanog modula približno je velik kao izvorni tekst uvećan za dvobajtno zaglavlje po bloku od 4096 bajtova, a ne manji kao potpuno tokenizirani blok. Svaki čitač koji implementira dekompresiju prema specifikaciji, uključujući Excel, i dalje ispravno otvara rezultat jer je sirovi blok jednako valjan CompressedChunk kao i blok komprimiran tokenima

Što HotXLS ne mijenja pri prepisivanju modula

Rekompresija uvijek zamjenjuje samo dio toka modula. Svaki tok modula najprije pohranjuje predmemoriju performansi, a zatim komprimirani izvor, dok tok projekta dir u zapisu MODULEOFFSET točno bilježi gdje se ta podjela nalazi za svaki modul; HotXLS čita taj pomak, ostavlja svaki bajt prije njega potpuno nepromijenjenim i ponovno gradi samo komprimirani spremnik od tog pomaka nadalje

Sam izvorni tekst prolazi kroz vlastitu kodnu stranicu VBA projekta, a ne kroz UTF-8 — istu naslijeđenu kodnu stranicu kojom je Office izvorno zapisao projekt. Ako izmjena SourceCode uvede znakove izvan skupa te kodne stranice, HotXLS ih pri ponovnom kodiranju niza u bajtove tiho zamijeni zamjenskim znakovima prema najboljem podudaranju, umjesto da prijavi pogrešku, pa je neobičan regionalni znak u komentaru ili znakovnom literal-u najvjerojatnije mjesto na kojem će se gubitak primijetiti. Vanjske reference i povezivanja biblioteka u istom projektu slijede srodan, ali zaseban put očuvanja, opisan u pratećem članku o očuvanju vanjskih VBA veza, koji vrijedi pročitati prije prolaza prepisivanja nad projektom povezanim s drugim radnim knjigama ili bibliotekama tipova

Kako se prepisane makronaredbe vraćaju u radnu knjigu

Nijedan poziv ne pokreće korak rekompresije izričito — on se automatski izvršava čim se spremi radna knjiga ili zaseban VBA projekt. TXLSVBAProject.ApplyChanges prolazi kroz svaki modul, ponovno komprimira one čiji se SourceCode promijenio od posljednjeg spremanja i ponovno zapisuje samo tok tog modula; klasični TXLSWorkbook.SaveAs, kada cilj spremanja zadržava izvorni format datoteke, i OOXML TXLSXWorkbook.SaveAs za XLSM paket s makronaredbama oba ga pozivaju interno prije zapisivanja bilo čega na disk, a SaveVBAProjectToFile poziva istu metodu kada je cilj zasebna datoteka VBA projekta, a ne cijela radna knjiga

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;

Sva tri odredišta ispod dijele istu mehaniku SourceCode i ApplyChanges; jedina stvarna razlika među njima jest koji poziv spremanja na kraju pokrene rekompresiju

Gdje ovo i dalje može zakazati

Dva su načina kvara dovoljno česta da ih treba planirati prije prolaza prepisivanja nad produkcijskim datotekama. Digitalno potpisan VBA projekt prestaje biti valjano potpisan čim se njegov izvor promijeni jer potpis obuhvaća sadržaj projekta; HotXLS ga ne može ponovno potpisati u vaše ime, a Excel će pri sljedećem otvaranju datoteke potpis ukloniti ili označiti kao nevažeći, pa potpisani projekt s makronaredbama treba naknadni korak ponovnog potpisivanja ako ga vaš tijek rada doista provjerava. Drugi se način kvara odnosi na svakoga tko bi pokušao ispočetka ponovno implementirati ovaj format kompresije umjesto da upotrijebi biblioteku koja ga već podržava: samo jedan pogrešan bit u zaglavlju bloka, polju potpisa, polju veličine ili zastavici kompresije proizvodi datoteku koju Excel odbija otvoriti, obično iza općeg upozorenja o oštećenju koje ne otkriva koji je bajt pogrešan — upravo takvu klasu pogrešaka izbjegava ranije opisana strategija zapisivanja sirovih blokova

Za upotrebu svega navedenog nije potrebno obrnuto inženjerstvo formata. Razvojni programeri za Delphi i C++Builder dobivaju pristup čitanju i pisanju svojstva SourceCode, rekompresiju usklađenu s MS-OVBA i sva tri ovdje opisana odredišta ponovnog zapisivanja kao dio standardne komponente HotXLS komponenta, zajedno s ostatkom njezina API-ja za klasične XLS i OOXML radne knjige