Mengganti nama sebuah referensi worksheet yang hardcode di seribu template laporan macro-enabled meniadakan pilihan membuka setiap file di VBA editor dengan tangan. HotXLS, komponen Excel native Delphi dan C++Builder, menangani kasus itu dengan mengekspos source sebuah modul VBA sebagai properti SourceCode yang bisa diedit dan mengompres ulang setiap edit dengan algoritma kompresi MS-OVBA yang didefinisikan Microsoft untuk penyimpanan VBA, menulis hasilnya kembali ke penyimpanan VBA XLS klasik, sebuah file proyek VBA mandiri, atau sebuah workbook XLSM macro-enabled. Tidak ada instance Excel, tidak ada VBA editor, dan tidak ada macro recorder yang terlibat di mana pun dalam jalur itu
Mengapa sebuah stream modul VBA bukan file teks
Sebuah modul VBA di dalam sebuah workbook XLS atau sebuah file proyek VBA mandiri bukan teks source yang duduk di sebuah stream menunggu untuk dibaca — ia adalah sebuah container biner kecil. Sebuah cache performa terkompilasi datang lebih dulu, byte yang digunakan Office untuk melewati kompilasi ulang modul saat load ketika cache masih cocok dengan versi host, dan teks source sesungguhnya mengikuti, dijalankan lewat sebuah skema kompresi proprietary yang didefinisikan MS-OVBA secara khusus untuk penyimpanan VBA. Skema itu bukan zip, bukan deflate, dan bukan apa pun yang dihasilkan API kompresi Windows secara native, yang persis mengapa kebanyakan library Excel pihak ketiga bisa membaca source sebuah modul — dekompresi adalah separuh masalah yang lebih mudah — sambil berhenti sebelum menulisnya kembali, karena rekompresi adalah tempat sebuah bit yang salah secara halus menghasilkan sebuah file yang ditolak Excel untuk dibuka. Tulisan publik tentang sisi baca ada; implementasi sisi-tulis yang benar-benar menjalankan rekompresi, bukan sekadar membongkar sebuah modul yang sudah ada untuk inspeksi, cukup langka sehingga ini tetap menjadi salah satu sudut paling jarang terdokumentasi dari format file Excel
Apa sebenarnya yang diubah properti SourceCode milik HotXLS?
HotXLS merepresentasikan setiap modul VBA sebagai sebuah objek TXLSVBAModule dengan properti SourceCode: WideString polos, dan menetapkan nilai baru padanya persis sesederhana kelihatannya: modul itu ditandai kotor di memori, dan tidak ada apa pun yang menyentuh stream OLE di baliknya sampai proyek disimpan. Proyek itu sendiri berasal dari IXLSWorkbook.VBAProject pada mesin XLS klasik atau TXLSXWorkbook.ParsedVBAProject pada mesin OOXML macro-enabled, keduanya mengembalikan sebuah TXLSVBAProject yang modul-modulnya duduk di balik sebuah indexer Item[] berbasis-1 dan sebuah properti Count, sehingga sebuah edit batch di seluruh modul dalam sebuah workbook sekadar sebuah loop di atas sebuah rentang integer
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;
Loop itu juga merupakan bentuk sebuah langkah audit. Sebelum seribu template disentuh, kebanyakan tim ingin lebih dulu tahu berapa banyak di antaranya yang benar-benar membawa makro dan apa yang direferensikan makro tersebut, yang merupakan skenario di balik workbench audit dan konversi workbook — Project.Count yang sama yang menggerakkan sebuah loop penulisan ulang di sini menjadi sebuah tally makro per-file di sana
Di dalam container kompresi MS-OVBA
Format kompresi MS-OVBA membungkus byte source ke dalam apa yang disebut spesifikasi sebagai CompressedContainer: sebuah byte signature tunggal, yang harus sama dengan 0x01, diikuti sebuah urutan blok CompressedChunk, masing-masing mencakup hingga 4096 byte data terdekompresi. Sebuah header chunk 16-bit membawa tiga field — sebuah signature 3-bit yang harus sama dengan 3, sebuah field ukuran 12-bit, dan sebuah bit CompressedChunkFlag yang menandai apakah payload chunk tersebut adalah byte literal atau sebuah urutan terkompresi-token. Ketika flag itu diset, payload-nya adalah sebuah rangkaian grup delapan token yang diawali-flag-byte, dan setiap token baik sebuah byte literal tunggal atau sebuah CopyToken: sebuah back-reference offset/panjang ke byte yang sudah didekompresi lebih awal di chunk yang sama, dengan lebar bit yang terbagi antara offset dan panjang bergeser bergantung seberapa jauh ke dalam chunk decompressor saat ini berada. Bagian MS-OVBA ini (§2.4.1, Compression and Decompression) adalah tempat sebuah implementasi buatan tangan paling sering kehilangan satu hari untuk sebuah off-by-one dalam perhitungan lebar-bit itu
Mengapa HotXLS menulis chunk mentah alih-alih mencocokkan token
Jalur tulis HotXLS sepenuhnya menghindari separuh pencocokan-token dari algoritma tersebut. Ketika ia mengompres ulang sebuah modul yang diedit, setiap chunk keluar dengan CompressedChunkFlag dikosongkan, artinya chunk itu memegang byte literal alih-alih token back-reference — sah di bawah MS-OVBA, karena sebuah container terkompresi diizinkan seluruhnya terdiri dari chunk tidak-terkompresi, dan ini menghilangkan persis bagian dari algoritma yang paling sulit dilakukan dengan benar secara manual: menemukan back-reference yang valid dan mengemas sebuah pasangan offset/panjang ke dalam sebuah lebar bit yang bergantung pada posisi saat ini di dalam chunk. Trade-off-nya muncul dalam ukuran file, bukan kebenaran — sebuah stream modul yang ditulis ulang mendarat dekat dengan ukuran teks source-nya plus sebuah header dua-byte per blok 4096-byte, tidak lebih kecil seperti sebuah chunk yang sepenuhnya terkompresi-token akan menjadi. Setiap reader yang mengimplementasikan sisi dekompresi spesifikasi, Excel termasuk, tetap membuka hasilnya dengan benar, karena sebuah chunk mentah sama validnya sebagai sebuah CompressedChunk dengan yang terkompresi-token
Apa yang dibiarkan tidak tersentuh HotXLS ketika ia menulis ulang sebuah modul
Rekompresi hanya pernah menggantikan sebagian stream modul. Setiap stream modul menyimpan cache performa-nya lebih dulu dan source terkompresi-nya kedua, dan stream dir proyek mencatat persis di mana pemisahan itu jatuh untuk setiap modul dalam sebuah entry MODULEOFFSET; HotXLS membaca offset itu, mempertahankan setiap byte sebelumnya persis seperti ditemukannya, dan membangun ulang hanya container terkompresi dari offset itu ke depan
Teks source itu sendiri melakukan round-trip lewat code page proyek VBA sendiri alih-alih UTF-8 — code page lawas yang sama yang digunakan Office menulis proyek tersebut sejak awal. Sebuah edit SourceCode yang memperkenalkan karakter di luar repertoire code page itu diam-diam disubstitusi dengan karakter pengganti best-fit ketika HotXLS meng-encode ulang string tersebut kembali ke byte, tidak ditolak, sehingga sebuah karakter regional yang tidak biasa yang dimasukkan ke sebuah komentar atau string literal adalah tempat paling mungkin untuk memperhatikan kehilangan itu. Referensi eksternal dan binding library di dalam proyek yang sama mengikuti sebuah jalur preservasi yang berkaitan tetapi terpisah, dibahas di artikel pendamping tentang preservasi link eksternal VBA, dan layak dibaca sebelum sebuah langkah penulisan-ulang menyentuh sebuah proyek yang terhubung keluar ke workbook lain atau type library
Bagaimana cara mengembalikan makro yang ditulis ulang ke dalam sebuah workbook?
Tidak ada yang memanggil langkah rekompresi secara eksplisit — ia berjalan otomatis begitu sebuah workbook atau sebuah proyek VBA mandiri disimpan. TXLSVBAProject.ApplyChanges menelusuri setiap modul, mengompres ulang yang SourceCode-nya berubah sejak penyimpanan terakhir, dan menulis ulang hanya stream modul itu; TXLSWorkbook.SaveAs klasik, ketika target penyimpanan mempertahankan format asli file, dan TXLSXWorkbook.SaveAs OOXML untuk sebuah paket XLSM macro-enabled keduanya memanggilnya secara internal sebelum apa pun ditulis ke disk, dan SaveVBAProjectToFile memanggil metode yang sama ketika target adalah sebuah file proyek VBA terlepas alih-alih sebuah workbook penuh
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;
Ketiga tujuan itu berbagi mekanika SourceCode dan ApplyChanges yang sama di baliknya; satu-satunya perbedaan sesungguhnya di antara mereka adalah pemanggilan simpan mana yang akhirnya memicu rekompresi
Di mana ini masih rusak
Dua mode kegagalan cukup umum untuk direncanakan sebelum sebuah langkah penulisan-ulang berjalan terhadap file produksi. Sebuah proyek VBA yang ditandatangani secara digital berhenti tervalidasi tanda tangannya begitu source-nya berubah, karena tanda tangan itu mencakup konten proyek; HotXLS tidak memiliki cara untuk menandatangani ulang sebuah proyek atas nama Anda, dan Excel membuang atau menandai tanda tangan itu saat file berikutnya dibuka, sehingga sebuah proyek makro yang ditandatangani membutuhkan sebuah langkah penandatanganan-ulang hilir jika tanda tangan itu adalah sesuatu yang benar-benar diperiksa workflow Anda. Mode kegagalan kedua milik siapa pun yang tergoda mengimplementasikan ulang format kompresi ini dari nol alih-alih menggunakan sebuah library yang sudah menanganinya: satu bit yang salah dalam sebuah header chunk, dalam nibble signature, field ukuran, atau flag terkompresi, menghasilkan sebuah file yang ditolak Excel untuk dibuka, biasanya di balik sebuah peringatan korupsi generik yang tidak memberi petunjuk byte mana yang salah — persis kelas bug yang ingin dihindari strategi penulisan chunk-mentah yang dijelaskan sebelumnya
Tidak satu pun dari ini membutuhkan reverse-engineering format tersebut untuk digunakan. Developer Delphi dan C++Builder mendapatkan akses baca dan tulis SourceCode, rekompresi yang sesuai MS-OVBA, dan ketiga tujuan tulis-ulang yang dijelaskan di sini sebagai bagian dari HotXLS Component standar, berdampingan dengan sisa API workbook XLS klasik dan OOXML-nya