مقاله فنی

بازنویسی سورس VBA و فشرده‌سازی مجدد MS-OVBA در Delphi

تغییرنام یک ارجاع سخت‌کدشده‌ی برگ‌کاری در سراسر هزار الگوی گزارش ماکرو-فعال، بازکردن هر فایل در ویرایشگر VBA دستی را از رده خارج می‌کند. HotXLS، کامپوننت بومی Excel در Delphi و C++Builder، آن حالت را با در معرض دید گذاشتن سورس یک ماژول VBA به‌عنوان یک ویژگی SourceCode قابل‌ویرایش و فشرده‌سازی مجدد هر ویرایش با الگوریتم فشرده‌سازی MS-OVBA که مایکروسافت برای storage از VBA تعریف می‌کند، مدیریت می‌کند، و نتیجه را به storage از VBA در XLS کلاسیک، یک فایل مستقل پروژه‌ی VBA، یا یک کاربرگ ماکرو-فعال XLSM بازمی‌نویسد. هیچ نمونه‌ی اکسل، هیچ ویرایشگر VBA، و هیچ ضبط‌کننده‌ی ماکرویی جایی در آن مسیر درگیر نیست

چرا یک جریان ماژول VBA یک فایل متنی نیست

یک ماژول VBA درون یک کاربرگ XLS یا یک فایل مستقل پروژه‌ی VBA سورس متنی نیست که درون یک جریان نشسته و منتظر خوانده‌شدن باشد — یک کانتینر باینری کوچک است. یک کش کارایی کامپایل‌شده اول می‌آید، بایت‌هایی که Office برای رد‌شدن از دوباره‌کامپایل‌کردن ماژول در بارگذاری وقتی کش هنوز با نسخه‌ی میزبان مطابقت دارد استفاده می‌کند، و متن سورس واقعی به‌دنبال آن می‌آید، از طریق یک طرح فشرده‌سازی اختصاصی عبور کرده که MS-OVBA به‌طور مشخص برای storage از VBA تعریف می‌کند. آن طرح zip نیست، deflate نیست، و چیزی نیست که APIهای فشرده‌سازی ویندوز بومی تولید کنند، که دقیقاً همان دلیلی است که اغلب کتابخانه‌های شخص ثالث Excel می‌توانند سورس یک ماژول را بخوانند — رمزگشایی نیمه‌ی آسان‌تر مسئله است — درحالی‌که پیش از بازنویسی آن متوقف می‌شوند، چون فشرده‌سازی مجدد جایی است که یک بیت به‌ظرافت اشتباه یک فایل تولید می‌کند که اکسل از بازکردنش سر باز می‌زند. نوشته‌های عمومی درباره‌ی سمت خواندن وجود دارند؛ پیاده‌سازی‌های سمت نوشتن که واقعاً فشرده‌سازی مجدد را اعمال می‌کنند، نه صرفاً بازکردن یک ماژول موجود برای بازرسی، به‌اندازه‌ی کافی کمیاب هستند که این یکی از کم‌مستندترین گوشه‌های قالب‌های فایل اکسل بماند

ویژگی SourceCode در HotXLS واقعاً چه چیزی را تغییر می‌دهد؟

HotXLS هر ماژول VBA را به‌عنوان یک شیء TXLSVBAModule با یک ویژگی ساده‌ی SourceCode: WideString نمایش می‌دهد، و انتساب یک مقدار جدید به آن دقیقاً به‌همان‌سادگی است که به‌نظر می‌رسد: ماژول در حافظه به‌عنوان کثیف علامت می‌خورد، و هیچ چیزی جریان OLE زیرین را لمس نمی‌کند تا زمانی که پروژه ذخیره شود. خودِ پروژه از IXLSWorkbook.VBAProject روی موتور XLS کلاسیک یا TXLSXWorkbook.ParsedVBAProject روی موتور ماکرو-فعال OOXML می‌آید، هر دو یک TXLSVBAProject برمی‌گردانند که ماژول‌هایش پشت یک اندیس‌گذار مبنا-۱ Item[] و یک ویژگی Count می‌نشینند، پس یک ویرایش دسته‌ای در سراسر هر ماژول در یک کاربرگ صرفاً یک حلقه روی یک بازه‌ی عدد صحیح است

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;

آن حلقه هم شکل یک پاس ممیزی است. پیش از اینکه هزار الگو لمس شوند، اغلب تیم‌ها اول می‌خواهند بدانند چند تا از آن‌ها واقعاً ماکرو حمل می‌کنند و آن ماکروها به چه چیزی ارجاع می‌دهند، که همان سناریوی پشت کارگاه ممیزی و تبدیل کاربرگ است — همان Project.Countای که یک حلقه‌ی بازنویسی را اینجا هدایت می‌کند، آنجا به یک شمارش ماکرو به‌ازای هر فایل تبدیل می‌شود

درون کانتینر فشرده‌سازی MS-OVBA

قالب فشرده‌سازی MS-OVBA بایت‌های سورس را در چیزی که مشخصات آن را CompressedContainer می‌نامد بسته‌بندی می‌کند: یک بایت امضای تکی، که باید برابر 0x01 باشد، به‌دنبال آن یک دنباله از بلوک‌های CompressedChunk، هرکدام پوشش‌دهنده تا ۴۰۹۶ بایت داده‌ی رمزگشایی‌شده. یک هدر بلوک ۱۶بیتی سه فیلد حمل می‌کند — یک امضای ۳بیتی که باید برابر ۳ باشد، یک فیلد اندازه‌ی ۱۲بیتی، و یک بیت CompressedChunkFlag که نشانه‌گذاری می‌کند آیا payload بلوک بایت‌های لفظی است یا یک دنباله‌ی توکن-فشرده‌شده. وقتی پرچم تنظیم شود، payload یک دنباله از گروه‌های پیشوندشده‌با-بایت-پرچم از هشت توکن است، و هر توکن یا یک بایت لفظی تکی است یا یک CopyToken: یک ارجاع-برگشتی افست/طول به بایت‌هایی که از پیش قبل‌تر در همان بلوک رمزگشایی شده‌اند، با عرض بیتی تقسیم‌شده بین افست و طول که بسته به اینکه رمزگشا در حال حاضر چقدر داخل بلوک نشسته جابه‌جا می‌شود. این بخش از MS-OVBA (§2.4.1، فشرده‌سازی و رمزگشایی) جایی است که یک پیاده‌سازی دستی‌نوشته اغلب یک روز را در یک خطای یکی-کم در آن محاسبه‌ی عرض بیت از دست می‌دهد

چرا HotXLS بلوک‌های خام می‌نویسد به‌جای تطبیق توکن

مسیر نوشتن HotXLS کاملاً نیمه‌ی تطبیق-توکن آن الگوریتم را دور می‌زند. وقتی یک ماژول ویرایش‌شده را فشرده‌سازی مجدد می‌کند، هر بلوک با CompressedChunkFlag پاک‌شده بیرون می‌رود، به‌معنای اینکه بلوک بایت‌های لفظی نگه می‌دارد نه توکن‌های ارجاع-برگشتی — قانونی تحت MS-OVBA، چون یک کانتینر فشرده مجاز است کاملاً از بلوک‌های فشرده‌نشده تشکیل شود، و این دقیقاً همان بخش از الگوریتم را حذف می‌کند که سخت‌ترین بخش برای دستی-درست‌گرفتن است: یافتن ارجاع‌های-برگشتی معتبر و بسته‌بندی یک جفت افست/طول درون عرض بیتی که به موقعیت فعلی درون بلوک بستگی دارد. این معامله در اندازه‌ی فایل نمایان می‌شود، نه در درستی — یک جریان ماژول بازنویسی‌شده نزدیک به اندازه‌ی متن سورش به‌علاوه‌ی یک هدر دوبایتی به‌ازای هر بلوک ۴۰۹۶بایتی فرود می‌آید، نه کوچک‌تر همان‌طور که یک بلوک کاملاً توکن-فشرده‌شده می‌بود. هر خواننده‌ای که سمت رمزگشایی مشخصات را پیاده‌سازی می‌کند، اکسل هم شامل، همچنان نتیجه را به‌درستی باز می‌کند، چون یک بلوک خام دقیقاً به‌همان‌اندازه یک CompressedChunk معتبر است که یک نوع توکن-فشرده‌شده

HotXLS چه چیزی را دست‌نخورده رها می‌کند وقتی یک ماژول را بازمی‌نویسد

فشرده‌سازی مجدد فقط تا به‌حال بخشی از جریان ماژول را جایگزین می‌کند. هر جریان ماژول اول کش کارایی‌اش را ذخیره می‌کند و دوم سورس فشرده‌شده‌اش را، و جریان dir پروژه دقیقاً ثبت می‌کند آن تقسیم کجا برای هر ماژول در یک ورودی MODULEOFFSET می‌افتد؛ HotXLS آن افست را می‌خواند، هر بایت پیش از آن را دقیقاً همان‌طور که یافته نگه می‌دارد، و فقط کانتینر فشرده را از آن افست به بعد بازمی‌سازد

خودِ متن سورس از طریق code page خودِ پروژه‌ی VBA رفت‌وبرگشت می‌کند نه UTF-8 — همان code page قدیمی‌ای که Office با آن پروژه را در وهله‌ی اول نوشته. یک ویرایش SourceCode که نویسه‌هایی خارج از قلمرو آن code page معرفی کند، بی‌سروصدا با نویسه‌های جایگزین بهترین-تطابق جایگزین می‌شود وقتی HotXLS رشته را دوباره به بایت رمزگذاری می‌کند، نه رد می‌شود، پس یک نویسه‌ی منطقه‌ای غیرمعمول که در یک کامنت یا رشته‌ی لفظی افتاده باشد، محتمل‌ترین جا برای متوجه‌شدن این افت است. ارجاع‌های خارجی و پیوندهای کتابخانه‌ای درون همان پروژه از یک مسیر حفاظتی مرتبط اما جداگانه پیروی می‌کنند، که در مقاله‌ی همراه درباره‌ی حفظ لینک‌های خارجی VBA پوشش داده شده، و ارزش خواندن دارد پیش از اینکه یک پاس بازنویسی پروژه‌ای را لمس کند که به کاربرگ‌ها یا کتابخانه‌های نوع دیگری لینک می‌دهد

ماکروهای بازنویسی‌شده را چطور به یک کاربرگ برمی‌گردانید؟

هیچ چیزی به‌طور صریح گام فشرده‌سازی مجدد را فرا نمی‌خواند — این به‌طور خودکار همان لحظه‌ای که یک کاربرگ یا یک پروژه‌ی VBA مستقل ذخیره می‌شود اجرا می‌شود. TXLSVBAProject.ApplyChanges هر ماژول را می‌پیماید، آن‌هایی را که SourceCodeاشان از آخرین ذخیره تغییر کرده فشرده‌سازی مجدد می‌کند، و فقط جریان آن ماژول را بازمی‌نویسد؛ هم TXLSWorkbook.SaveAs کلاسیک، وقتی هدف ذخیره قالب اصلی فایل را نگه می‌دارد، و هم TXLSXWorkbook.SaveAs از OOXML برای یک بسته‌ی ماکرو-فعال XLSM هر دو آن را داخلاً پیش از اینکه چیزی روی دیسک نوشته شود فرا می‌خوانند، و SaveVBAProjectToFile همان متد را وقتی هدف یک فایل پروژه‌ی VBA جدا‌شده به‌جای یک کاربرگ کامل است فرا می‌خواند

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;

هر سه مقصد همان مکانیک SourceCode و ApplyChanges را زیرِ خودشان به اشتراک می‌گذارند؛ تنها تفاوت واقعی بین آن‌ها این است که کدام فراخوانی ذخیره در نهایت فشرده‌سازی مجدد را ماشه می‌کشد

این کجا هنوز می‌شکند

دو حالت شکست به‌اندازه‌ی کافی رایج هستند که پیش از اینکه یک پاس بازنویسی روی فایل‌های تولیدی اجرا شود، برایشان برنامه‌ریزی کنید. یک پروژه‌ی VBA امضاشده‌ی دیجیتال همان لحظه‌ای که سورسش تغییر کند از اعتبار امضاشده‌بودنش می‌ایستد، چون امضا محتوای پروژه را پوشش می‌دهد؛ HotXLS هیچ راهی برای دوباره‌امضاکردن یک پروژه از طرف شما ندارد، و اکسل دفعه‌ی بعدی که فایل باز شود امضا را می‌اندازد یا پرچم می‌زند، پس یک پروژه‌ی ماکروی امضاشده به یک گام دوباره‌امضایی پایین‌دستی نیاز دارد اگر آن امضا چیزی است که گردش‌کار شما واقعاً بررسی می‌کند. حالت شکست دوم متعلق به هرکسی است که وسوسه شده این قالب فشرده‌سازی را از صفر دوباره پیاده‌سازی کند به‌جای استفاده از یک کتابخانه که از پیش آن را مدیریت می‌کند: یک بیت اشتباه تکی در یک هدر بلوک، در نیبل امضا، فیلد اندازه، یا پرچم فشرده، فایلی تولید می‌کند که اکسل از بازکردنش سر باز می‌زند، معمولاً پشت یک هشدار خرابی عمومی که هیچ سرنخی از اینکه کدام بایت اشتباه بوده نمی‌دهد — دقیقاً همان رده‌ی باگی که استراتژی نوشتن بلوک-خام که پیش‌تر توصیف شد برای اجتناب از آن وجود دارد

هیچ‌کدام از این‌ها به مهندسی‌معکوس این قالب برای استفاده‌کردن نیاز ندارد. توسعه‌دهندگان Delphi و C++Builder دسترسی خواندن و نوشتن SourceCode، فشرده‌سازی مجدد مطابق MS-OVBA، و هر سه مقصد بازنویسی توصیف‌شده در اینجا را به‌عنوان بخشی از کامپوننت استاندارد HotXLS می‌گیرند، در کنار بقیه‌ی API کاربرگ XLS کلاسیک و OOXML آن