HotXLS mendekode BIFF8 XLUnicodeString dengan membaca cch dan flag fHigh lebih dulu, kemudian memilih reader yang sesuai dengan encoding: TXLSBlob.GetWideString dengan byte count cch * 2 ketika fHigh bernilai 1, dan TXLSBlob.GetString ketika fHigh bernilai 0. Pasangkan keduanya secara terbalik dan record menghasilkan string kosong atau setengah panjang, bukan exception
Itulah yang membuat kelas bug ini mahal. Chart terbuka, series tergambar dengan benar, axis tepat, lalu satu trendline caption kosong begitu saja. Tidak ada apa pun di log, tidak ada apa pun di exception handler, dan tidak ada dialog corrupt-file. File sebenarnya baik-baik saja; reader hanya meminta jumlah byte yang salah dan mendapatkan tepat apa yang dimintanya
Mengapa string BIFF8 kembali kosong?
String BIFF8 kembali kosong karena length guard menolak payload sebelum read terjadi, atau karena reader berhenti pada NUL pertama yang ditemukannya. Kedua jalur ini diam-diam sejak awal. Di HotXLS, guard biasanya berupa pemeriksaan DataLength eksplisit di record handler, dan harus dihitung per encoding: payload 16-bit membutuhkan 8 + cch * 2 byte untuk body SXViewLink, sedangkan payload 8-bit hanya membutuhkan 8 + cch. Terapkan aritmetika wide-character pada record 8-bit dan setiap short name gagal melewati gate. Perilaku NUL adalah jebakan kedua, karena TXLSBlob.GetString dan TXLSBlob.GetWideString sama-sama memindai hasil decode untuk terminator dan memotong di sana, lalu mengembalikan string kosong ketika terminator berada di posisi pertama. Baca body 16-bit dengan setengah byte count dan Anda mempertahankan karakter pertama sebanyak cch div 2; baca body 8-bit melalui wide reader dan pasangan byte tersebut membentuk code point arbitrer. Hanya over-long read yang terdengar keras: TXLSBlob.EnsureReadable mengangkat Blob read exceeds data size ketika request melewati ukuran blob. Under-reading tidak memiliki alarm seperti itu
GetWideString menghitung byte, bukan karakter
TXLSBlob.GetWideString(Index, Count) menerima Count dalam byte. Secara internal method ini menjalankan SetString di atas PWideChar dengan Count div SizeOf(WideChar), sehingga memberikan character count secara diam-diam membagi string menjadi dua. Sementara itu, layout record BIFF8 menyatakan panjang string dalam karakter. Karena itu setiap call site 16-bit harus membawa konversi * 2 sendiri, dan setiap call site 8-bit tidak boleh membawanya. Ini adalah batas encoding yang sama yang muncul ketika Anda menulis teks kembali, bukan hanya membacanya, dan layak dibaca bersama Unicode-safe spreadsheet export di Delphi jika pipeline Anda memindahkan string ke dua arah
// 16-bit XLUnicodeStringNoCch: cch karakter, cch * 2 byte
Name := Data.GetWideString(Start, cch * 2); // benar
Name := Data.GetWideString(Start, cch); // setengah teks, tanpa error
// 8-bit XLUnicodeStringNoCch: cch karakter, cch byte
Name := WideString(Data.GetString(Start, cch)); // benar
Name := Data.GetWideStringWithZero(Start, cch); // tetap wide reader
Konvensi ini berlaku di mana pun byte stream ditelusuri secara manual. Ketika HotXLS menyatukan kembali String record panjang ($0207, [MS-XLS] 2.4.268) dari Continue record-nya ($003C), wide branch menghitung segCh dari segment length lalu memanggil GetWideString(3, segCh * 2), karena record body dimulai pada offset 3 dan count tetap berupa byte. Rich-text reader melakukan hal yang sama dari offset 1 pada Continue segment pertama. [MS-XLS] 2.5.293 menjamin bahwa break berada pada batas karakter double-byte ketika fHighByte bernilai 1, sehingga tidak diperlukan bookkeeping karakter parsial, tetapi aritmetika byte tetap harus Anda lakukan dengan benar
Apa yang sebenarnya dilakukan GetWideStringWithZero?
TXLSBlob.GetWideStringWithZero adalah wide-character reader yang mempertahankan embedded NUL. Suffix WithZero menandai retensi NUL, bukan lebar karakter: secara internal ia menjalankan SetString yang sama di atas PWideChar dengan Count div SizeOf(WideChar) seperti GetWideString, hanya tanpa terminator scan. Counterpart satu byte-nya adalah TXLSBlob.GetStringWithZero, yang mengembalikan AnsiString. Tidak ada bagian nama yang memberi tahu mana yang mana, dan ambiguitas itu telah menimbulkan bug nyata di codebase ini. Salah baca yang spesifik layak disebut karena terlihat sangat masuk akal: GetWideString membutuhkan cch * 2, jadi GetWideStringWithZero pasti yang menerima cch langsung. Method itu memang menerima cch tanpa complaint, memang mengembalikan WideString, dan compiler senang. Method itu juga mengembalikan setengah karakter, yang dirangkai dari pasangan byte yang salah. Jalur 8-bit yang benar adalah TXLSBlob.GetString dengan plain cch byte count, lalu di-cast ke WideString pada assignment. HotXLS 2.376.0 memperbaiki misuse tersebut tepat di dua chart decoder
SXViewLink dan length gate per encoding
SXViewLink ($0858, [MS-XLS] 2.4.316) adalah contoh kerja yang paling jelas karena mengemas kedua asimetri dalam header delapan byte. Layout-nya adalah rt(2), unused(2), reserved(2), cch(1), fHigh(1), lalu body XLUnicodeStringNoCch: fHigh = 1 berarti cch * 2 byte UTF-16, fHigh = 0 berarti cch byte karakter satu-byte, dan cch dibatasi 255 karena field length-nya satu byte. HotXLS menulis record ini ke chart globals sebelum Units, di sebelah PivotChartBits ($0859, [MS-XLS] 2.4.196), ketika chart sheet terhubung ke PivotTable view; sudut pandang tingkat record atas machinery itu dibahas dalam menulis BIFF8 PivotTable record di Delphi
// SXViewLink ([MS-XLS] 2.4.316): rt(2) unused(2) reserved(2) cch(1)
// lalu XLUnicodeStringNoCch - fHigh(1) diikuti karakter
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;
Kedua branch bukan kosmetik. Versi sebelumnya mengunci kedua encoding di balik expression wide-character 8 + cch * 2, sehingga view name 8-bit yang ditulis Excel gagal melewati guard dan decoder mengembalikan PivotSourceName kosong dengan IsPivotChart tetap false. Pivot link menghilang dari model tanpa satu pun diagnostic. Kesalahan identik berada di Trendline decoder ($2050, [MS-XLS] 2.4.328), ketika name field mengikuti payload numeric 28 byte dengan cch dua byte pada offset 28, fHigh pada offset 30, dan karakter pada offset 31; trendline caption yang ditulis Excel dengan karakter 8-bit ter-decode sebagai string kosong. Keduanya diperbaiki dalam release yang sama. Dan kasus 8-bit bukan keanehan legacy yang terbatas pada file Excel 2.0 sampai 4.0: Excel saat ini masih menulis payload BIFF8 8-bit ketika setiap karakter muat dalam satu byte
Bagaimana mendekode record BIFF8 baru dengan aman di Delphi?
Jika field tersebut benar-benar merupakan XLUnicodeString standar, gunakan TXLSBlob.GetBiffString, bukan membuat branch sendiri. Method ini membaca length field, membaca option byte, mengirim ke reader yang sesuai, lalu memajukan cursor melewati body. Dua parameter Boolean adalah bagian yang perlu dibaca teliti: is8bit menjelaskan lebar length field, bukan lebar karakter, dan iswide menyatakan apakah option byte fHigh ada sama sekali. BIFF version di bawah $0600 tidak memiliki keduanya
var
Offset: LongWord;
begin
Offset := 6; // pada SXViewLink byte cch dimulai di sini
// is8bit = length field lebarnya satu byte
// iswide = option byte fHigh mengikuti length field
Name := Data.GetBiffString(Offset, True, True);
// Offset kini menunjuk byte pertama setelah string body
Branch yang ditulis manual tetap berguna ketika handler harus bertahan menghadapi input yang terpotong atau hostile, karena GetBiffString mengandalkan EnsureReadable untuk mengangkat exception, bukan bounds check yang dapat Anda kendalikan. Itulah alasan chart decoder HotXLS melakukan gate pada DataLength dan mengembalikan nothing, bukan throw: workbook pihak ketiga yang malformed seharusnya hanya mengorbankan satu caption, bukan seluruh dokumen. Trade-off ini disengaja, dan tepat itulah sebabnya encoding-specific guard harus benar, karena guard tersebut yang mengubah bad read menjadi silence
Satu bagian proses terakhir, yang dipelajari dengan susah payah pada release yang sama. Lakukan assertion pada workbook yang sudah disimpan dan dibuka ulang, bukan pada in-memory model yang baru saja Anda bangun. Batch 2.376.0 juga menemukan emitter SXEx ([MS-XLS] 2.4.282) yang mendeklarasikan body 24 byte tetapi hanya menulis 22, sehingga setiap record setelah PivotTable view menjadi misaligned, termasuk worksheet EOF dan substream chart sheet apa pun yang mengikutinya. Test pivot yang ada tidak pernah menangkapnya karena semuanya melakukan assertion terhadap memory. String decoding memiliki sifat yang sama: round trip melalui file adalah satu-satunya test yang benar-benar menjalankan byte count
Jika Anda bekerja dengan internal classic XLS di Delphi atau C++Builder dan tidak ingin memelihara BIFF8 record reader sendiri, aturan encoding di atas sudah diimplementasikan dan diuji regresi dalam HotXLS Delphi spreadsheet component, yang membaca dan menulis XLS serta XLSX tanpa Excel atau OLE automation apa pun