يفك HotXLS سلسلة XLUnicodeString في BIFF8 بقراءة cch وعلم fHigh أولًا، ثم يختار القارئ المطابق للترميز: TXLSBlob.GetWideString بعدد بايتات يساوي cch * 2 عندما تكون قيمة fHigh هي 1، وTXLSBlob.GetString عندما تكون قيمة fHigh هي 0. اعكس اقترانهما، فيعيد السجل سلسلة فارغة أو نصف الطول، ولا يصدر أي استثناء
وهذا ما يجعل فئة الأخطاء هذه مكلفة. يفتح مخطط، وتُرسم السلاسل بصورة صحيحة، وتكون المحاور سليمة، ويصبح عنوان trendline واحد فارغًا ببساطة. لا شيء في السجل، ولا شيء في معالج الاستثناء، ولا مربع حوار يشير إلى ملف تالف. كان الملف سليمًا طوال الوقت؛ فقد طلب القارئ عددًا خاطئًا من البايتات وحصل بالضبط على ما طلبه
لماذا تعود سلسلة BIFF8 فارغة؟
تعود سلسلة BIFF8 فارغة لأن حارس الطول رفض الحمولة قبل حدوث القراءة، أو لأن القارئ توقف عند أول NUL وجده. وكلا المسارين صامت بحكم البناء. ففي HotXLS يكون الحارس عادة فحصًا صريحًا لـDataLength داخل معالج السجل، وعليه أن يُحسب لكل ترميز: تحتاج حمولة 16 بت إلى 8 + cch * 2 بايتات لجسم SXViewLink، بينما تحتاج حمولة 8 بت إلى 8 + cch فقط. طبّق حساب المحارف العريضة على سجل 8 بت، وسيفشل كل اسم قصير في الحاجز. أما سلوك NUL فهو الفخ الثاني، لأن TXLSBlob.GetString وTXLSBlob.GetWideString يفحصان النتيجة المفكوكة عن منهي ويقتطعان عنده، فيعيدان سلسلة فارغة عندما يقع المنهي في الموضع الأول. اقرأ جسمًا 16 بت بنصف عدد البايتات، فتحافظ على أول cch div 2 محرف؛ واقرأ جسمًا 8 بت عبر القارئ العريض، فتكوّن أزواج البايتات نقاط ترميز عشوائية. والقراءة الأطول من اللازم هي الوحيدة الصاخبة: إذ يرفع TXLSBlob.EnsureReadable العبارة Blob read exceeds data size عندما يتجاوز الطلب حجم blob. أما القراءة الأقصر فلا تملك هذا المنبه
يعد GetWideString البايتات لا المحارف
يأخذ TXLSBlob.GetWideString(Index, Count) قيمة Count بالبايتات. وينفذ داخليًا SetString فوق PWideChar باستخدام Count div SizeOf(WideChar)، ولذلك يؤدي تمرير عدد المحارف إلى تقصير السلسلة إلى النصف بصمت. وفي المقابل تعبر تخطيطات سجلات BIFF8 عن طول السلسلة بالمحارف. لذلك يجب على كل موضع استدعاء 16 بت أن يحمل تحويل * 2 بنفسه، وعلى كل موضع 8 بت ألا يحمله. وهذه هي حدود الترميز نفسها التي تظهر عند كتابة النص مرة أخرى بدل قراءته، ومن المفيد قراءتها مع تصدير جداول البيانات الآمن لـUnicode في Delphi إذا كان مسارك ينقل السلاسل في الاتجاهين
// XLUnicodeStringNoCch ذي 16 بت: cch محرف، وcch * 2 بايت
Name := Data.GetWideString(Start, cch * 2); // صحيح
Name := Data.GetWideString(Start, cch); // نصف النص، بلا خطأ
// XLUnicodeStringNoCch ذي 8 بت: cch محرف، وcch بايت
Name := WideString(Data.GetString(Start, cch)); // صحيح
Name := Data.GetWideStringWithZero(Start, cch); // لا يزال قارئًا عريضًا
ويصمد الاصطلاح في كل موضع يجري فيه اجتياز تدفق البايتات يدويًا. فعندما يصل HotXLS سجل String طويلًا ($0207، [MS-XLS] 2.4.268) من سجلات Continue ($003C) الخاصة به، يحسب الفرع العريض segCh من طول المقطع، ثم يستدعي GetWideString(3, segCh * 2) لأن جسم السجل يبدأ عند الإزاحة 3 ولأن العدد لا يزال بايتات. وينفذ قارئ rich-text الشيء نفسه من الإزاحة 1 في مقطع Continue الأول. وتضمن [MS-XLS] 2.5.293 أن يقع الكسر على حد محرف ثنائي البايت عندما تكون fHighByte قيمتها 1، ولذلك لا حاجة إلى حفظ محرف جزئي، لكن حساب البايتات لا يزال مسؤوليتك لتصحيحه
ماذا يفعل GetWideStringWithZero فعلًا؟
إن TXLSBlob.GetWideStringWithZero قارئ محارف عريضة يحافظ على NULs المضمّنة. وتدل لاحقة WithZero على الاحتفاظ بـNUL لا على عرض المحرف: فهو يشغل داخليًا SetString فوق PWideChar باستخدام Count div SizeOf(WideChar) مثل GetWideString، لكن من دون فحص المنهي. أما النظير ذو البايت الواحد فهو TXLSBlob.GetStringWithZero الذي يعيد AnsiString. ولا يخبر الاسم بأي منهما هو أي، وقد كلف هذا الالتباس قاعدة الكود أخطاء حقيقية. ويستحق سوء الفهم المحدد التسمية لأنه يبدو معقولًا جدًا: GetWideString يحتاج cch * 2، إذن لا بد أن يكون GetWideStringWithZero هو الذي يأخذ cch مباشرة. وهو يأخذ cch بلا شكوى، ويعيد WideString، ويكون المصرّف سعيدًا. لكنه يعيد أيضًا نصف المحارف، مركبة من أزواج البايت الخطأ. والمسار الصحيح ذو 8 بت هو TXLSBlob.GetString مع عدد بايت عادي cch، ثم التحويل إلى WideString عند الإسناد. وقد أصلح HotXLS 2.376.0 سوء الاستخدام هذا بالضبط في مفككين للمخططات
SXViewLink وحاجز الطول الخاص بكل ترميز
يُعد SXViewLink ($0858، [MS-XLS] 2.4.316) أوضح مثال تطبيقي، لأنه يجمع عدمي التماثل في رأس واحد من ثمانية بايتات. فالتخطيط هو rt(2) وunused(2) وreserved(2) وcch(1) وfHigh(1)، ثم جسم XLUnicodeStringNoCch: تعني fHigh = 1 عدد cch * 2 من بايتات UTF-16، وتعني fHigh = 0 عدد cch من محارف البايت الواحد، وتُحصر cch عند 255 لأن حقل الطول بايت واحد. وتكتب HotXLS السجل في globals المخطط قبل Units، بجانب PivotChartBits ($0859، [MS-XLS] 2.4.196)، عندما ترتبط ورقة مخطط بعرض PivotTable؛ وتغطي كتابة سجلات PivotTable لـBIFF8 في Delphi رؤية السجلات لهذه الآلية
// SXViewLink ([MS-XLS] 2.4.316): rt(2) unused(2) reserved(2) cch(1)
// ثم XLUnicodeStringNoCch - fHigh(1) يتبعه المحارف
PivCch := Item.FData.GetByte(6);
if (PivCch > 0) and (Item.FData.GetByte(7) <> 0) and
(Item.FData.DataLength >= LongWord(8 + PivCch * 2)) then
begin
Result.PivotSourceName := Item.FData.GetWideString(8, PivCch * 2);
Result.IsPivotChart := True;
end
else if (PivCch > 0) and (Item.FData.GetByte(7) = 0) and
(Item.FData.DataLength >= LongWord(8 + PivCch)) then
begin
Result.PivotSourceName := WideString(Item.FData.GetString(8, PivCch));
Result.IsPivotChart := True;
end;
الفرعان ليسا زينة. فقد وضعت نسخة سابقة كلا الترميزين خلف تعبير المحارف العريضة 8 + cch * 2، ولذلك فشل اسم عرض ذي 8 بت كتبه Excel في اجتياز الحاجز، وأعاد المفكك PivotSourceName فارغًا مع بقاء IsPivotChart بالقيمة false. واختفى رابط pivot من النموذج من دون أي تشخيص. وكان الخطأ نفسه موجودًا في مفكك Trendline ($2050، [MS-XLS] 2.4.328)، حيث يأتي حقل الاسم بعد 28 بايتًا من الحمولة الرقمية مع cch من بايتين عند الإزاحة 28، وfHigh عند الإزاحة 30، والمحارف عند الإزاحة 31؛ فالعناوين التي كتبها Excel بمحارف 8 بت فُكّت إلى سلاسل فارغة. وأُصلح الاثنان في الإصدار نفسه. كما أن حالة 8 بت ليست فضولًا قديمًا محصورًا في ملفات Excel 2.0 إلى 4.0: فما زال Excel الحالي يكتب حمولات BIFF8 ذات 8 بت كلما أمكن تمثيل كل محرف في بايت واحد
كيف تفك سجل BIFF8 جديدًا بأمان في Delphi؟
عندما يكون الحقل فعلًا XLUnicodeString قياسيًا، استخدم TXLSBlob.GetBiffString بدل كتابة الفرع يدويًا. فهي تقرأ حقل الطول، وتقرأ بايت الخيارات، وتوجه إلى القارئ المطابق، ثم تقدم المؤشر بعد الجسم. والمعاملان المنطقيان هما الجزء الذي ينبغي قراءته بعناية: فـis8bit يصف عرض حقل الطول لا عرض المحارف، وiswide يقول هل يوجد بايت خيار fHigh أصلًا. ولا يملك إصدارات BIFF تحت $0600 أيًا منهما
var
Offset: LongWord;
begin
Offset := 6; // في SXViewLink يبدأ بايت cch هنا
// is8bit = حقل الطول بعرض بايت واحد
// iswide = يتبع حقل الطول بايت خيار fHigh
Name := Data.GetBiffString(Offset, True, True);
// يشير Offset الآن إلى أول بايت بعد جسم السلسلة
ولا تزال الفروع المكتوبة يدويًا جديرة بمكانها عندما يجب على المعالج تحمل إدخال مبتور أو عدائي، لأن GetBiffString تعتمد على رفع EnsureReadable بدل فحص حدود تتحكم فيه. وهذا هو سبب وضع مفكك مخطط HotXLS شرطًا على DataLength وإعادته لا شيء بدل الرمي: إذ ينبغي أن يكلف مصنف مشوه من طرف ثالث عنوانًا واحدًا لا المستند كله. والمقايضة مقصودة، وهي بالضبط سبب ضرورة صحة الحاجز الخاص بالترميز، لأن الحاجز هو الشيء الذي يحول القراءة السيئة إلى صمت
وتبقى قطعة أخيرة من العملية، تعلمناها بالطريقة الصعبة في الإصدار نفسه. أكد على مصنف محفوظ ومعاد فتحه، لا على النموذج الموجود في الذاكرة الذي بنيته للتو. فقد كشفت دفعة 2.376.0 أيضًا مُصدّر SXEx ([MS-XLS] 2.4.282) أعلن جسمًا من 24 بايتًا وكتب 22 فقط، فأخرج كل سجل بعد عرض PivotTable عن محاذاته، بما فيه EOF الخاص بورقة العمل وأي تدفق فرعي لورقة مخطط يأتي بعده. ولم تلتقط الاختبارات الموجودة لـpivot ذلك لأنها أكدت كلها على الذاكرة. ولفك السلاسل الخاصية نفسها: فالمرور ذهابًا وإيابًا عبر الملف هو الاختبار الوحيد الذي يشغل حساب البايتات فعلًا
إذا كنت تعمل مع الأجزاء الداخلية لـXLS التقليدي في Delphi أو C++Builder ولا تريد صيانة قارئ BIFF8 خاص بك، فإن قواعد الترميز أعلاه منفذة ومختبرة باختبارات التراجع في مكوّن جداول HotXLS لـDelphi، الذي يقرأ ويكتب XLS وXLSX من دون Excel أو أي أتمتة OLE