یک 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 تنزل برمیگردد
چرا مارکر 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 جاسازیشده در مسیر کدگذاریشده
چرا 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 فرود میآیند
دستهبندی یک فرمول پیش از منجمد کردنش
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 روی ماشینی که کار میکند