افتح ملف xls قديما ثم أعد حفظه، فإذا بصيغة الوظيفة الإضافية التي كانت تستدعي مكتبة تحليل مسجلة تشير الآن إلى مرجع فارغ داخل المصنف نفسه. يتعقب HotXLS ذلك الفساد الصامت حتى يصل إلى افتراض خاطئ واحد: أن سجل BIFF SupBook إما self أو ملف خارجي. يحدد [MS-XLS] سبعة أنواع لا اثنين
لماذا يفقد المصنف المحفوظ روابط وظائفه الإضافية؟
لأن اختبار التصنيف كان بنيويا بدلا من أن يكون نوعيا. الاختصار التقليدي يقرأ سجل SupBook ($01AE)، ويتحقق مما إذا كان يحمل علامة self، وإن لم يكن كذلك يعامل أي سلسلة تليها كعنوان URL لمستند. كل سجل ليس أيّا من هذين الاثنين يسقط في فرع افتراضي، والفرع الافتراضي هو في الغالب «هذا هو المصنف نفسه». الرابط الداعم للوظيفة الإضافية، ورابط نفس الورقة، والخانة غير المستخدمة، والسجل المقتطع، كلها تنتهي حاملة الملصق الخاطئ نفسه. لا شيء يثير استثناء أثناء حدوث ذلك: السجل حُلّل، والصيغة أُعيد ترجمتها، والملف حُفظ دون تحذير، ويطفو العيب بعد ثلاثة أسابيع حين يلاحظ أحدهم عمودا من الأصفار حيث كان تحويل عملة. يصف [MS-XLS] §2.4.271 سجلا يمكن أن يكون مرجعا ذاتيا، أو مرجعا لنفس الورقة، أو حاوية دوال وظيفة إضافية، أو مصنفا خارجيا بمسار افتراضي وجدول أسماء أوراق، أو رابط بيانات DDE أو OLE، أو عنصرا نائبا غير مستخدم — بالإضافة إلى حالة سابعة ليست في المواصفة لكنها موجودة على أقراص حقيقية، وهي السجل الذي لا يُحلَّل. الإصلاح ليس طريقة حدسية أفضل؛ بل رفض وجود أي طريقة حدسية إطلاقا
الأنواع السبعة التي قد يحملها سجل SupBook
يعلن HotXLS تصنيف الروابط الداعمة كتعداد مغلق في lxExternSheet.pas، وكل قرار لاحق يتفرع عليه. تسع قيم تعداد تغطي الفئات السبع، لأن حالة DDE وOLE تحتاج حالة مؤقتة قبل أن تُحسم:
type
TXLSSupportingLinkKind = (
slkUnknown, // فشل التحليل، أو بقيت بايتات زائدة
slkSelf, // هذا المصنف
slkSameSheet, // علامة U+0000
slkAddIn, // حاوية دوال الوظيفة الإضافية
slkExternalWorkbook, // مسار افتراضي + جدول أسماء الأوراق
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. وعدد أوراق يساوي واحدا مقترنا بـ$3A01 يميز حاوية وظيفة إضافية. فقط قيمة في المدى من 1 إلى $00FF تعني أن مسارا افتراضيا مشفرا يليها، وعندها فقط يفك HotXLS ترميز سلسلة على الإطلاق. أي شيء خارج هذه الأشكال الثلاثة يبقى slkUnknown، والسجل الذي لا يستهلك جدول أسماء أوراقه جسم السجل بالضبط يُخفَّض إلى slkUnknown حتى لو بدت مقدمته معقولة
لماذا تُفك علامة نفس الورقة كسلسلة فارغة؟
لأن قارئ سلاسل BIFF العام يُتلف البايت الذي يعتمد عليه التصنيف. الرابط الداعم لنفس الورقة هو سلسلة من حرف واحد وحرفها الوحيد هو U+0000، ويعيد TXLSBlob.GetBiffString ذلك كـWideString فارغة، لا يمكن تمييزها عن مسار فارغ حقيقة — وهو تماما المدخل الذي تجيبه الطريقة الحدسية للمرجع الذاتي بـ«self». لذلك يقرأ HotXLS نقطة الكود الأولى الخام من جسم السجل بدلا من الوثوق بالقيمة المفكوكة:
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); // عريض، بايتان
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;
لاحظ فرع المضغوط مقابل العريض. بايت الخيارات يقع عند إزاحة ثابتة من رأس السلسلة، ونقطة الكود الأولى تكون بايتا واحدا أو اثنين حسب البت 0، لذا قراءتها كبايت دون قيد تعمل على معظم الملفات وتفشل على تلك التي كتبتها إصدارات مترجمة — وهو أسوأ توزيع ممكن لخلل. يُمسَك بالعنصر النائب غير المستخدم بالطريقة نفسها، عبر حمولته الحرفية المكونة من مسافة واحدة، وبحالة 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 تحويل الفرق بواحد مرة واحدة بالضبط، عند النقطة التي يدخل فيها الرمز المميز إلى شجرة الصياغة الداخلية، ولا مكان غيرها. PtgNameX.ixti ([MS-XLS] §2.5.198.85) فهرس ذو أساس صفري في مصفوفة rgXTI لسجل ExternSheet ($0017، §2.4.106)، بينما اصطلاح ExternID الداخلي في المكتبة ذو أساس أحادي مع حجز الصفر لـ«لا ورقة خارجية». مسار قراءة BIFF8 ينفذ FExternID := wValue + 1 حين يفك رمز tNameX، ومسار الكتابة يُصدر StoreExternID - 1، تاركا عرض الرمز الخام ودلالات القرص دون مساس. الخطأ هنا صعب الاكتشاف بشكل غير معتاد: الأسماء المعرفة الخارجية تُحَل إلى المدخل المجاور، وفي ملف بمدخل XTI واحد يصبح الفهرس 0 هو الفهرس 1 فيخطئ، ويتدهور الاسم بصمت. اختبار الانحدار الذي يمرّن فقط نص الصيغة المعاد ترجمته لا يراه إطلاقا، لأن إعادة الترجمة لا تلمس فهرس القرص أصلا — وهو الفخ نفسه الذي يجعل الأسماء المعرفة الممتدة عبر الأوراق والمصنفات جديرة بالاختبار ضد تدفقات بايت حقيقية. الحلّ محدود من الطرفين: TlxExternSheetSheet.TryResolveXti يُرجع False لفهرس سالب أو مدخل مفقود، وTXLSSupBook.TryGetKind يُرجع False لفهرس SupBook خارج المصفوفة، ثم يربط ClassifyXti كلا من slkSelf وslkSameSheet بـfrcInternal، وslkExternalWorkbook بـfrcExternalWorkbook، وslkAddIn وslkDde وslkOle وslkDdeOrOle بـfrcExternalOther. كل ما عدا ذلك، بما في ذلك كل مسار خارج المدى، يقع على frcUnknownOrMalformed
تصنيف الصيغة قبل تجميدها
يفحص TXLSCompiledFormula.ClassifyReferences تدفق رموز BIFF المحفوظ مباشرة بدلا من تفكيك الصيغة والبحث عن أقواس مربعة. صيد الأقواس في نص الصيغة هو طريقة حدسية نصية ترتدي معطف محلل: فهي تطابق السلاسل الحرفية، وتطابق المراجع البنيوية، وتغفل الأسماء المعرفة الخارجية تماما، لأنها لا تحمل أقواسا في الصيغة المفككة. فحص الرموز ينظر فقط إلى 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 فقط؛
// المراجع الداخلية والوظائف الإضافية وDDE/OLE والمشوّهة تبقى صيغا
Sheet.ConvertFormulasToValues(True);
end;
Wb.SaveAs('quarterly-detached.xls');
finally
Wb.Free;
end;
end;
المعامل OnlyExternal هو حيث يثمر التصنيف. تجميد الصيغة لا رجعة فيه، لذا على العملية أن تُثبت أن المرجع مصنف خارجي بدلا من مجرد الاشتباه به. استدعاءات الوظائف الإضافية تنجو، روابط DDE وOLE تنجو، وأي شيء لم يستطع المحلل فهمه بالكامل ينجو، لأن النتيجة الآمنة لعدم اليقين هي عدم تغيير شيء. الانضباط نفسه يحكم إعادة ربط الصيغ المنسوخة بين المصنفات، حيث يُعاد ربط المرجع المصنَّف خطأ إلى المصنف الخطأ بدلا من الفشل بصوت عال
السجلات التي يتعذر تحليلها تُعاد كتابتها كما هي
يحتفظ HotXLS بحمولة SupBook الأصلية ويعيد إصدارها بايتا ببايت حين لا يُحرَّر السجل إطلاقا. فشل التحليل يضبط slkUnknown ويمسح الحالة المشتقة، لكن الجسم الملتقط يبقى في FRawData، ويفضله مسار التخزين على أي إعادة بناء ما دام العنصر غير متسخ وليس سجل self. البديل — تطبيع سجل غير مُحلَّل إلى مرجع ذاتي كي يجد الكاتب شيئا جيد التشكيل ليصدره — يحوّل سجلا لم تفهمه إلى سجل خاطئ بشكل قاطع. المبدأ نفسه هو العقد المطبق على مشاريع VBA ومراجعها الخارجية عبر دورة تحميل وحفظ، وهو الفرق بين مكتبة تحافظ على ملفات العالم الحقيقي ذهابا وإيابا وأخرى تحافظ فقط على الملفات التي تصادف احتواء مجموعة اختباراتها عليها. المصنف الذي مر عبر خمسة عشر عاما من إصدارات Excel ومولّد تقارير وأداتي ترحيل سيحوي سجلات لم يصممها حيّ اليوم. أعد كتابتها كما وجدتها
شُحن التصنيف النوعي لسجلات SupBook وXTI في HotXLS من الإصدار 2.361.2 حتى 2.361.4، مع حلّ XTI المحدود ومسار ConvertFormulasToValues الأكثر أمانا الموصوف هنا. إن كنت تحافظ على كود Delphi أو C++Builder يقرأ ملفات xls قديمة تحمل استدعاءات وظائف إضافية أو روابط DDE أو OLE أو أسماء معرفة خارجية، فإن مكوّن HotXLS لجداول البيانات في Delphi يعالج التصنيف كله أصليا، دون تثبيت Excel ودون أتمتة OLE على الجهاز الذي يقوم بالعمل