مقاله فنی

دسته‌بندی لینک‌های خارجی BIFF SupBook و XTI در Delphi

یک xls قدیمی باز کنید، دوباره ذخیره‌اش کنید، و فرمول add-in ای که به یک کتابخانه تحلیل ثبت‌شده صدا زده بود حالا به یک ارجاع خالی داخل خود کتاب‌کار اشاره می‌کند. HotXLS آن خرابی بی‌سروصدا را به یک فرض بد می‌رساند: اینکه یک رکورد BIFF SupBook یا self است یا یک فایل خارجی. [MS-XLS] هفت نوع تعریف می‌کند، نه دو تا

چرا کتاب‌کار ذخیره‌شده لینک‌های add-in خودش را گم می‌کند؟

چون آزمون دسته‌بندی ساختاری بود نه تایپ‌محور. میان‌بُر سنتی رکورد SupBook ($01AE) را می‌خواند، چک می‌کند مارکر self را حمل می‌کند یا نه، و اگر نه، هر رشته‌ای که بعدش بیاید را یک URL سند می‌گیرد. هر رکوردی که هیچ‌کدام از این دو نباشد به شاخه پیش‌فرض می‌افتد و شاخه پیش‌فرض تقریباً همیشه «این خود کتاب‌کار است» است. یک لینک پشتیبان add-in، یک لینک same-sheet، یک اسلات بلااستفاده و یک رکورد ناقص همگی در نهایت همان برچسب غلط را می‌پوشند. هیچ استثنایی وقتی این اتفاق می‌افتد پرتاب نمی‌شود: رکورد تجزیه شد، فرمول دوباره کامپایل شد، فایل بدون هشدار ذخیره شد، و نقص سه هفته بعد ظاهر می‌شود وقتی کسی متوجه ستونی از صفرها می‌شود جایی که قبلاً تبدیل ارز بود. [MS-XLS] §2.4.271 رکوردی را توصیف می‌کند که می‌تواند یک self-reference باشد، یا یک ارجاع same-sheet، یا یک ظرف تابع add-in، یا یک کتاب‌کار خارجی با مسیر مجازی و جدول نام sheet، یا یک لینک داده DDE یا OLE، یا یک جای‌نگهدار بلااستفاده — و یک حالت هفتم که در مشخصه نیست ولی روی دیسک‌های واقعی وجود دارد: رکوردی که تجزیه نمی‌شود. راه‌حل یک هیوریستیک بهتر نیست؛ امتناع از داشتن هر هیوریستیکی است

هفت نوعی که یک رکورد SupBook می‌تواند حمل کند

HotXLS تاکسونومی لینک پشتیبان را به‌شکل یک شمارش بسته در lxExternSheet.pas اعلام می‌کند و هر تصمیم پایین‌دستی روی آن سوئیچ می‌شود. نه مقدار شمارش هفت دسته را پوشش می‌دهند، چون مورد DDE و OLE پیش از حل شدن به یک حالت موقت نیاز دارد:

type
  TXLSSupportingLinkKind = (
    slkUnknown,           // تجزیه ناموفق، یا بایت‌های انتهایی باقی مانده بود
    slkSelf,              // همین کتاب‌کار
    slkSameSheet,         // مارکر U+0000
    slkAddIn,             // ظرف تابع add-in
    slkExternalWorkbook,  // مسیر مجازی + جدول نام sheet
    slkDde,               // حل‌شده از فلگ‌های ExternName
    slkOle,               // حل‌شده از فلگ‌های ExternName
    slkDdeOrOle,          // یکی از این دو، هنوز معلوم نیست کدام
    slkUnused);           // جای‌نگهدار تک‌فاصله

  TXLSFormulaReferenceClass = (
    frcInternal,
    frcExternalWorkbook,
    frcExternalOther,
    frcUnknownOrMalformed);

  TXLSXtiInfo = record
    XtiIndex    : Integer;   // صفر-مبنا، همان‌طور که در ExternSheet.rgXTI ذخیره می‌شود
    ExternID    : Integer;   // یک-مبنا، قرارداد داخلی
    SupBookIndex: Integer;
    Sheet1Index : Integer;
    Sheet2Index : Integer;
    LinkKind    : TXLSSupportingLinkKind;
  end;

مسیریابی سنتینل‌محور است نه رشته‌محور. مقدار فیلد $0401 رکورد self را علامت می‌زند. شمار sheet برابر یک همراه با $3A01 یک ظرف add-in را علامت می‌زند. فقط مقداری در بازه 1 تا $00FF یعنی یک مسیر مجازی کدگذاری‌شده در ادامه می‌آید، و فقط در آن صورت است که HotXLS اصلاً یک رشته دیکد می‌کند. هر چیز بیرون از این سه شکل slkUnknown می‌ماند و رکوردی که جدول نام sheet‌اش بدنه رکورد را دقیقاً مصرف نکند حتی وقتی سرش محتمل به نظر می‌رسید به slkUnknown تنزل برمی‌گردد

نردبان سنتینل‌محوری که HotXLS برای دسته‌بندی یک رکورد BIFF SupBook به هفت نوع استفاده می‌کند؛ رشته فقط برای مقادیر در بازه مسیر کدگذاری‌شده دیکد می‌شود و به‌جای شاخه پیش‌فرض به یک نوع ناشناخته برمی‌گردد
هر نوع با یک سنتینل به دست می‌آید نه با یک آزمون رشته‌ای، و رکوردی که هیچ‌کدام از شکل‌ها را نگیرد ناشناخته می‌ماند به‌جای افتادن در شاخه پیش‌فرضی که یعنی همین کتاب‌کار

چرا مارکر same-sheet به‌شکل یک رشته خالی دیکد می‌شود؟

چون خواننده رشته عمومی BIFF بایتی را که دسته‌بندی به آن وابسته است نابود می‌کند. لینک پشتیبان same-sheet یک رشته تک‌کاراکتری است که تک‌کاراکترش U+0000 است و TXLSBlob.GetBiffString آن را به‌شکل یک WideString خالی برمی‌گرداند، بی‌تمایز از یک مسیر واقعاً خالی — دقیقاً همان ورودی‌ای که یک هیوریستیک self-reference به آن جواب «self» می‌دهد. HotXLS بنابراین اولین code point خام را از بدنه رکورد می‌خواند نه اینکه به مقدار دیکدشده اعتماد کند:

StringOffset := offset;
FDocUrl := Data.GetBiffString(offset, False, True);
FirstChar := $FFFF;
if val = 1 then
begin
  StringOptions := Data.GetByte(StringOffset + 2);
  if (StringOptions and $01) = 0 then
    FirstChar := Data.GetByte(StringOffset + 3)     // فشرده، یک بایت
  else
    FirstChar := Data.GetWord(StringOffset + 3);    // wide، دو بایت
end;

if FirstChar = 0 then
  FKind := slkSameSheet
else if (Length(FDocUrl) = 1) and (FDocUrl[1] = WideChar(#32)) then
  FKind := slkUnused
else if Pos(WideChar(#3), FDocUrl) > 0 then
  FKind := slkDdeOrOle
else if FDocUrl <> '' then
  FKind := slkExternalWorkbook;

به شاخه compressed در مقابل wide دقت کنید. بایت آپشن در یک آفست ثابت از هدر رشته می‌نشیند و اولین code point بسته به بیت 0 یک بایت است یا دو، پس خواندنش به‌عنوان بایت به‌طور بی‌قید و شرط روی بیشتر فایل‌ها کار می‌کند و روی فایل‌های نوشته‌شده توسط بیلدهای localized شکست می‌خورد — بدترین توزیع ممکن برای یک باگ. جای‌نگهدار بلااستفاده هم به همان شکل گرفته می‌شود، با payload لفظی تک‌فاصله‌اش، و مورد DDE یا OLE با جداکننده U+0003 جاسازی‌شده در مسیر کدگذاری‌شده

چرا HotXLS اولین code point خام را از بدنه رکورد BIFF SupBook می‌خواند نه رشته دیکدشده را، چون خواننده رشته عمومی مارکر U+0000 در same-sheet را به یک مقدار خالی تبدیل می‌کند
مارکر same-sheet یک رشته تک‌کاراکتری است که کاراکترش U+0000 است، پس خواننده رشته عمومی آن را در یک مقدار خالی می‌تاپد و فقط code point خام در آفست بایت آپشن آن را نگه می‌دارد

چرا DDE و OLE در زمان SupBook از هم جدا نمی‌شوند؟

چون رکورد SupBook بیت‌های متمایزکننده را حمل نمی‌کند. به شما می‌گوید لینک یکی از این دو است؛ فلگ‌های fOle و fOleLink که تعیین می‌کنند کدام، در رکورد ExternName ($0023) زندگی می‌کنند که بعداً در استریم می‌رسد. HotXLS در زمان تجزیه slkDdeOrOle را ثبت می‌کند و در ParseExternalName آن را تنگ‌تر می‌کند، و اگر هرگز ExternName نرسد نوع برای همیشه موقت می‌ماند — که درست است، چون فایل واقعاً نمی‌گوید. هر مصرف‌کننده پایین‌دستی آن مقدار موقت را یک مقدار واقعی می‌گیرد نه یک مقدار غایب، پس هیچ فراخوانی مجبور نیست یک تسویه‌حساب ابداع کند. حدس زدن «احتمالاً DDE» این‌جا یک شمارش مرتب‌تر می‌خرد و یک کلاس جواب غلط که هیچ‌کس نمی‌تواند ردیابی‌اش کند:

if FKind = slkDdeOrOle then
begin
  if Data.DataLength < 2 then
    Exit;
  Flags := Data.GetWord(0);
  if (Flags and $0010) <> 0 then
    FKind := slkOle
  else if (Flags and $0008) <> 0 then
    FKind := slkDde;
end;

ایندکس‌های XTI روی دیسک صفر-مبنا و در داخل یک-مبنایند

HotXLS تبدیل off-by-one را دقیقاً یک بار انجام می‌دهد، در نقطه‌ای که یک توکن وارد درخت سینتکس داخلی می‌شود، و هیچ‌جای دیگر. PtgNameX.ixti ([MS-XLS] §2.5.198.85) یک ایندکس صفر-مبنا داخل آرایه rgXTI رکورد ExternSheet ($0017، §2.4.106) است، در حالی که قرارداد ExternID داخلی کتابخانه یک-مبناست با صفر محفوظ برای «بدون sheet خارجی». مسیر خواندن BIFF8 هنگام دیکد کردن توکن tNameX مقدار FExternID := wValue + 1 را انجام می‌دهد و مسیر نوشتن StoreExternID - 1 را منتشر می‌کند و نمای خام توکن و معناشناسی روی‌دیسک دست‌نخورده می‌ماند. اشتباه گرفتن این به‌طور غیرمعمولی سخت پیدا می‌شود: نام‌های تعریف‌شده خارجی به مدخل مجاور حل می‌شوند و در فایلی با یک مدخل XTI، ایندکس 0 می‌شود ایندکس 1، خطا می‌خورد و نام بی‌سروصدا تنزل می‌کند. رگرسیونی که فقط متن فرمول دوباره‌کامپایل‌شده را ورزش می‌دهد هرگز نمی‌بیندش، چون دوباره‌کامپایل اصلاً ایندکس دیسک را دست نمی‌زند — همان تله‌ای که نام‌های تعریف‌شده روی sheetها و کتاب‌کارها را ارزش آزمودن در برابر استریم‌های بایت واقعی می‌کند. حل شدن در هر دو سر کران‌دار است: TlxExternSheetSheet.TryResolveXti برای ایندکس منفی یا مدخل غایب False برمی‌گرداند، TXLSSupBook.TryGetKind برای ایندکس SupBook بیرون از آرایه False برمی‌گرداند و ClassifyXti بعد slkSelf و slkSameSheet را به frcInternal نگاشت می‌کند، slkExternalWorkbook را به frcExternalWorkbook و slkAddIn و slkDde و slkOle و slkDdeOrOle را به frcExternalOther. بقیه چیزها، شامل هر مسیر بیرون از بازه، روی frcUnknownOrMalformed فرود می‌آیند

HotXLS ایندکس صفر-مبنای XTI یک توکن PtgNameX در BIFF را در یک نقطه واحد به ExternID داخلی یک-مبنایش تبدیل می‌کند، با حل کران‌دار در هر دو سر و نقشه دسته‌بندی که آن را مصرف می‌کند
off-by-one میان ایندکس صفر-مبنای دیسک و ExternID داخلی یک-مبنا یک بار اعمال می‌شود، وقتی توکن وارد درخت سینتکس می‌شود، و هر ایندکس حل‌نشدنی روی کلاس ناقص فرود می‌آید

دسته‌بندی یک فرمول پیش از منجمد کردنش

TXLSCompiledFormula.ClassifyReferences استریم توکن BIFF نگه‌داشته‌شده را مستقیماً اسکن می‌کند به‌جای دیکامپایل کردن فرمول و جستجوی کروشه. شکار کروشه در متن فرمول یک هیوریستیک متنی است که پالت تجزیه‌گر پوشیده: با رشته‌های literal مچ می‌شود، با ارجاع‌های structured مچ می‌شود و نام‌های تعریف‌شده خارجی را کاملاً از دست می‌دهد، چون آن‌ها در فرم دیکامپایل‌شده هیچ کروشه‌ای ندارند. اسکن توکن فقط به PtgNameX و PtgRef3d و PtgArea3d و PtgRefErr3d و PtgAreaErr3d نگاه می‌کند و وقتی هیچ استریم BIFF‌ای باقی نمانده به یک پیمایش درخت سینتکس برمی‌گردد. ادغام عمداً بدبینانه است — اولویت ثابت frcUnknownOrMalformed است، بعد frcExternalWorkbook، بعد frcExternalOther و بعد frcInternal — پس یک توکن ناخوانا کل فرمول را مسموم می‌کند. برای یک نام تعریف‌شده خارجی ایندکس نام هم اعتبارسنجی می‌شود: یک-مبنا، در بازه، و پشتوانه‌شده با یک رکورد ExternName نگه‌داشته‌شده

var
  Wb   : TXLSWorkbook;
  Sheet: TXLSWorksheet;
  i    : Integer;
begin
  Wb := TXLSWorkbook.Create;
  try
    Wb.Open('quarterly.xls');
    for i := 1 to Wb.Sheets.Count do        // Sheets یک-مبناست
    begin
      Sheet := Wb.Sheets[i];
      // فقط فرمول‌های دسته‌بندی‌شده frcExternalWorkbook را منجمد می‌کند؛
      // ارجاع‌های داخلی، add-in، DDE/OLE و ناقص فرمول می‌مانند
      Sheet.ConvertFormulasToValues(True);
    end;
    Wb.SaveAs('quarterly-detached.xls');
  finally
    Wb.Free;
  end;
end;

پارامتر OnlyExternal جایی است که تاکسونومی هزینه‌اش را درمی‌آورد. منجمد کردن یک فرمول برگشت‌ناپذیر است، پس عملیات باید اثبات کند یک ارجاع یک کتاب‌کار خارجی است نه صرفاً به آن مظنون باشد. فراخوانی‌های add-in جان سالم می‌برند، لینک‌های DDE و OLE جان سالم می‌برند و هر چیزی که تجزیه‌گر نتوانست کامل بفهمد جان سالم می‌برد، چون نتیجه امن عدم قطعیت تغییر ندادن هیچ چیز است. همان نظم بر اتصال دوباره فرمول‌های کپی‌شده میان کتاب‌کارها هم حاکم است، جایی که یک ارجاع بددسته‌بندی‌شده به کتاب غلط متصل می‌شود به‌جای آن‌که با سر و صدا شکست بخورد

رکوردهایی که تجزیه نمی‌شوند دست‌نخورده نوشته می‌شوند

HotXLS payload اصلی SupBook را نگه می‌دارد و وقتی رکورد هرگز ویرایش نشده آن را بایت به بایت دوباره منتشر می‌کند. یک شکست تجزیه slkUnknown را ست می‌کند و حالت استخراج‌شده را پاک می‌کند، اما بدنه ضبط‌شده در FRawData می‌ماند و مسیر ذخیره تا وقتی آیتم dirty نیست و رکورد self نیست، آن را به هر بازسازی ترجیح می‌دهد. جایگزین — نرمال کردن یک رکورد تجزیه‌نشده به یک self-reference تا نویسنده چیزی خوش‌فرم برای انتشار داشته باشد — رکوردی را که نفهمیدید به رکوردی قطعاً غلط تبدیل می‌کند. آن اصل همان قراردادی است که به پروژه‌های VBA و ارجاع‌های خارجی‌شان در یک چرخه لود-و-ذخیره اعمال می‌شود و تفاوت میان کتابخانه‌ای است که فایل‌های دنیای واقعی را round-trip می‌کند و کتابخانه‌ای که فایل‌هایی را round-trip می‌کند که مجموعه تستش اتفاقاً دارد. کتاب‌کاری که از پانزده سال نسخه‌های Excel، یک مولد گزارش و دو ابزار مهاجرت گذشته حاوی رکوردهایی خواهد بود که هیچ‌کس زنده فعلی طراحی‌شان نکرده. همان‌طور که پیدایشان کردید بنویسیدشان

دسته‌بندی تایپ‌محور رکوردهای SupBook و XTI در HotXLS 2.361.2 تا 2.361.4 عرضه شد، همراه با حل کران‌دار XTI و مسیر امن‌تر ConvertFormulasToValues توصیف‌شده در این مقاله. اگر کد Delphi یا C++Builder نگهداری می‌کنید که فایل‌های xls قدیمی حامل فراخوانی‌های add-in یا لینک‌های DDE یا OLE یا نام‌های تعریف‌شده خارجی را می‌خواند، HotXLS Delphi spreadsheet component کل تاکسونومی را به‌صورت نیتیو هندل می‌کند، بدون نصب Excel و بدون اتوماسیون OLE روی ماشینی که کار می‌کند