HotXLS, BIFF8 XLUnicodeString'i önce cch ve fHigh bayrağını okuyarak decode eder, sonra encoding'e uygun reader'ı seçer: fHigh 1 olduğunda cch * 2 byte count ile TXLSBlob.GetWideString, fHigh 0 olduğunda TXLSBlob.GetString. İkisini ters eşleştirirseniz record empty veya yarı uzunlukta bir string verir, hiç exception vermez
Bu bug sınıfını pahalı yapan da budur. Chart açılır, series doğru çizilir, eksenler doğrudur ve bir trendline caption yalnızca boştur. Günlükte hiçbir şey, exception handler'da hiçbir şey ve corrupt-file dialog yoktur. Dosya başından beri iyiydi; reader yanlış sayıda bayt istedi ve tam istediğini aldı
BIFF8 string neden boş döner?
BIFF8 string boş döner, çünkü bir length guard payload'ı okuma gerçekleşmeden önce reddetmiştir veya reader bulduğu ilk NUL'da durmuştur. Her iki yol da tasarım gereği sessizdir. HotXLS'te guard genellikle record handler içindeki açık bir DataLength kontrolüdür ve encoding başına hesaplanmalıdır: 16 bitlik payload bir SXViewLink body'si için 8 + cch * 2 bayta, 8 bitlik payload yalnızca 8 + cch bayta ihtiyaç duyar. Wide-character arithmetic'i 8 bitlik bir record'a uygularsanız her kısa ad kapıya takılır. NUL davranışı ikinci tuzaktır; TXLSBlob.GetString ve TXLSBlob.GetWideString decoded sonucu terminator için tarar ve orada keser, terminator birinci konuma düşerse empty string döndürür. 16 bitlik body'yi byte count'ın yarısıyla okursanız ilk cch div 2 karakteri tutarsınız; 8 bitlik body'yi wide reader üzerinden okursanız byte çiftleri rastgele code point'lere dönüşür. Yalnızca fazla uzun okuma seslidir: TXLSBlob.EnsureReadable, istek blob'ı aşınca Blob read exceeds data size yükseltir. Eksik okuma için böyle bir alarm yoktur
GetWideString karakter değil bayt sayar
TXLSBlob.GetWideString(Index, Count), Count değerini bayt olarak alır. İçeride Count div SizeOf(WideChar) kullanarak bir PWideChar üzerinde SetString yapar; bu nedenle character count geçirmeniz string'i sessizce yarıya indirir. Oysa BIFF8 record layout'ları string length'i karakterle ifade eder. Her 16 bitlik call site bu * 2 dönüşümünü kendi taşımalı, her 8 bitlik call site ise onu taşımamalıdır. Bu, metni yeniden yazarken değil okurken de görülen aynı encoding sınırıdır; pipeline'ınız string'leri iki yönde taşıyorsa Delphi'de Unicode-safe spreadsheet export ile birlikte okunmaya değer
// 16-bit XLUnicodeStringNoCch: cch karakter, cch * 2 bayt
Name := Data.GetWideString(Start, cch * 2); // doğru
Name := Data.GetWideString(Start, cch); // metnin yarısı, hata yok
// 8-bit XLUnicodeStringNoCch: cch karakter, cch bayt
Name := WideString(Data.GetString(Start, cch)); // doğru
Name := Data.GetWideStringWithZero(Start, cch); // hâlâ wide reader
Convention, byte stream'in elle yüründüğü her yerde geçerlidir. HotXLS uzun bir String record'ını ($0207, [MS-XLS] 2.4.268) Continue record'larından ($003C) yeniden birleştirirken wide branch, segment length'ten segCh hesaplar ve ardından GetWideString(3, segCh * 2) çağırır; çünkü record body 3 ofsetinden başlar ve count hâlâ bayttır. Rich-text reader da ilk Continue segment'inde 1 ofsetinden aynı işi yapar. [MS-XLS] 2.5.293, fHighByte 1 olduğunda break'in double-byte character boundary üzerinde olduğunu garanti eder; bu yüzden partial-character bookkeeping gerekmez ama byte arithmetic'i doğru yapmak yine size kalır
GetWideStringWithZero gerçekte ne yapar?
TXLSBlob.GetWideStringWithZero, embedded NUL'ları koruyan bir wide-character reader'dır. WithZero suffix'i character width'ü değil NUL retention'ı belirtir: içeride GetWideString ile aynı şekilde, Count div SizeOf(WideChar) kullanarak PWideChar üzerinde SetString çalıştırır, yalnızca terminator scan'i yapmaz. Bir-byte karşılığı, AnsiString döndüren TXLSBlob.GetStringWithZero'dır. İsimde hangisinin hangisi olduğu yazmaz ve bu belirsizlik codebase'e gerçek hatalar mal oldu. Özel yanlış okumayı adlandırmak gerekir, çünkü çok makul görünür: GetWideString cch * 2 istiyorsa GetWideStringWithZero doğrudan cch alan olmalı. Gerçekten de complaint etmeden cch alır, bir WideString döndürür ve compiler mutludur. Ama yanlış byte çiftlerinden kurulmuş karakterlerin yarısını döndürür. Doğru 8 bitlik yol, düz cch byte count ile TXLSBlob.GetString çağırıp assignment sırasında WideString'e cast etmektir. HotXLS 2.376.0, iki chart decoder'ındaki bu misuse'ı tam olarak düzeltti
SXViewLink ve encoding başına length gate
SXViewLink ($0858, [MS-XLS] 2.4.316), iki asimetriyi tek bir sekiz baytlık header'a sığdırdığı için en temiz worked example'dır. Layout rt(2), unused(2), reserved(2), cch(1), fHigh(1) ve ardından XLUnicodeStringNoCch body'sidir: fHigh = 1, UTF-16'nın cch * 2 bayt olduğu; fHigh = 0, one-byte character'ların cch bayt olduğu anlamına gelir ve length field tek bayt olduğu için cch 255 ile sınırlıdır. HotXLS, chart sheet bir PivotTable view'a bağlandığında record'ı Units'in öncesinde, chart globals içinde PivotChartBits ($0859, [MS-XLS] 2.4.196) yanında yazar; bu mekanizmanın record-level görünümü Delphi'de BIFF8 PivotTable record'larını yazma yazısında ele alınır
// SXViewLink ([MS-XLS] 2.4.316): rt(2) unused(2) reserved(2) cch(1)
// ardından bir XLUnicodeStringNoCch - fHigh(1), karakterler onu izler
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;
İki branch kozmetik değildir. Önceki bir sürüm her iki encoding'i de wide-character ifadesi olan 8 + cch * 2 arkasına koyuyordu; bu nedenle Excel'in yazdığı 8 bitlik view name guard'ı geçemiyor ve decoder PivotSourceName'i boş, IsPivotChart'ı false bırakıyordu. Pivot link tek bir diagnostic olmadan modelden kayboluyordu. Aynı hata Trendline decoder'ında ($2050, [MS-XLS] 2.4.328) da duruyordu; orada name field 28 bayt numeric payload'ın ardından, 28 ofsetinde iki baytlık cch, 30 ofsetinde fHigh ve 31 ofsetinde karakterlerle gelir; Excel'in 8 bitlik karakterlerle yazdığı trendline caption'lar empty string olarak decode ediliyordu. İkisi de aynı release'de düzeltildi. 8 bitlik durum yalnızca Excel 2.0 ile 4.0 dosyalarına ait eski bir merak da değildir: bütün karakterler tek bayta sığıyorsa güncel Excel hâlâ 8 bitlik BIFF8 payload'ları yazar
Delphi'de yeni bir BIFF8 record'ı güvenle nasıl decode edilir?
Alan gerçekten standart bir XLUnicodeString olduğunda branch'i elle yazmak yerine TXLSBlob.GetBiffString kullanın. Length field'ı okur, option byte'ını okur, eşleşen reader'a dispatch eder ve cursor'u body'nin sonrasına ilerletir. İki Boolean parameter dikkatle okunması gereken yerdir: is8bit character genişliğini değil length field'ının genişliğini belirtir, iswide ise length field'dan sonra bir fHigh option byte bulunup bulunmadığını söyler. $0600'ın altındaki BIFF sürümlerinde ikisi de yoktur
var
Offset: LongWord;
begin
Offset := 6; // SXViewLink'te cch byte'ı burada başlar
// is8bit = length field bir byte genişliğindedir
// iswide = length field'ı bir fHigh option byte izler
Name := Data.GetBiffString(Offset, True, True);
// Offset artık string body'sinden sonraki ilk byte'ı gösterir
Elle yazılmış branch'ler, handler'ın kesilmiş veya hostile input'tan sağ çıkması gerektiğinde hâlâ yerini hak eder; çünkü GetBiffString, kontrol ettiğiniz bir bounds check yerine EnsureReadable'ın yükseltmesine dayanır. HotXLS chart decoder'ının DataLength üzerinden guard koyup throw etmek yerine hiçbir şey döndürmemesinin nedeni budur: bozuk bir üçüncü taraf workbook size bir caption'a mal olmalı, bütün belgeye değil. Ödün bilerek seçilmiştir ve encoding'e özel guard'ın doğru olmasının nedeni de tam olarak budur; kötü bir read'i sessizliğe çeviren şey guard'dır
Aynı release'den zor öğrenilen son bir süreç dersi var. Yeni kurduğunuz in-memory model üzerinde değil, kaydedilmiş ve yeniden açılmış bir workbook üzerinde assertion yapın. 2.376.0 batch'i ayrıca bir SXEx emitter'ının ([MS-XLS] 2.4.282) 24 baytlık body bildirip yalnızca 22 yazdığını, PivotTable view'dan sonraki worksheet EOF ve devamındaki chart sheet substream'i dahil her record'ı hizadan çıkardığını ortaya çıkardı. Mevcut pivot testleri bunu hiç yakalamadı, çünkü hepsi memory'ye assertion yapıyordu. String decode da aynı özelliğe sahiptir: byte count'ları gerçekten çalıştıran tek test dosya üzerinden round-trip'tir
Delphi veya C++Builder'da klasik XLS internals ile çalışıyor ve kendi BIFF8 record reader'ınızı sürdürmek istemiyorsanız yukarıdaki encoding kuralları, Excel veya herhangi bir OLE automation olmadan XLS ve XLSX okuyan ve yazan HotXLS Delphi spreadsheet component içinde zaten uygulanmış ve regresyon testinden geçirilmiştir