Bài viết kỹ thuật

Giải mã XLUnicodeString BIFF8 trong Delphi: cch và fHigh

HotXLS giải mã XLUnicodeString BIFF8 bằng cách đọc cch và flag fHigh trước, rồi chọn reader khớp encoding: TXLSBlob.GetWideString với byte count cch * 2 khi fHigh bằng 1, còn TXLSBlob.GetString khi fHigh bằng 0. Ghép ngược hai thứ đó, record sẽ cho string rỗng hoặc ngắn một nửa mà không bao giờ exception

Đó là lý do lớp bug này đắt. Chart mở được, series vẽ đúng, axis đúng, nhưng một trendline caption đơn giản là trống. Không có gì trong log, exception handler không thấy gì, không có corrupt-file dialog. File vốn hoàn toàn ổn; reader yêu cầu sai số byte và nhận chính xác thứ nó yêu cầu

Vì sao BIFF8 string trả về rỗng?

BIFF8 string trả về rỗng vì length guard reject payload trước khi read diễn ra, hoặc reader dừng ở NUL đầu tiên tìm thấy. Cả hai path đều im lặng theo thiết kế. Trong HotXLS, guard thường là check DataLength tường minh trong record handler và phải được tính theo từng encoding: payload 16-bit cần 8 + cch * 2 byte cho body SXViewLink, còn payload 8-bit chỉ cần 8 + cch. Áp arithmetic wide-character lên record 8-bit sẽ khiến mọi tên ngắn fail gate. Bẫy NUL là bẫy thứ hai, vì TXLSBlob.GetStringTXLSBlob.GetWideString đều scan kết quả decoded tìm terminator rồi truncate tại đó, trả string rỗng khi terminator ở position một. Đọc body 16-bit với nửa byte count sẽ giữ lại cch div 2 character đầu; đọc body 8-bit qua wide reader sẽ ghép các cặp byte thành code point tùy ý. Chỉ over-long read mới ồn ào: TXLSBlob.EnsureReadable raise Blob read exceeds data size khi request chạy quá blob. Under-read không có alarm như vậy

GetWideString đếm byte, không đếm character

TXLSBlob.GetWideString(Index, Count) nhận Count tính bằng byte. Bên trong nó gọi SetString trên PWideChar với Count div SizeOf(WideChar), nên truyền character count sẽ âm thầm làm string ngắn một nửa. Trong khi đó BIFF8 record layout biểu diễn string length theo character. Vì vậy mọi call site 16-bit phải tự mang conversion * 2, còn call site 8-bit thì không được mang nó. Đây là cùng encoding boundary xuất hiện khi bạn ghi text ra thay vì đọc vào, đáng đọc song song với spreadsheet export an toàn Unicode trong Delphi nếu pipeline của bạn đưa string theo cả hai hướng

// XLUnicodeStringNoCch 16-bit: cch character, cch * 2 byte
Name := Data.GetWideString(Start, cch * 2);        // đúng
Name := Data.GetWideString(Start, cch);            // nửa text, không lỗi

// XLUnicodeStringNoCch 8-bit: cch character, cch byte
Name := WideString(Data.GetString(Start, cch));    // đúng
Name := Data.GetWideStringWithZero(Start, cch);    // vẫn là wide reader

Convention này đúng ở mọi nơi byte stream được walk bằng tay. Khi HotXLS ghép một String record dài ($0207, [MS-XLS] 2.4.268) từ các Continue record ($003C), nhánh wide tính segCh từ segment length rồi gọi GetWideString(3, segCh * 2) vì record body bắt đầu ở offset 3 và count vẫn tính bằng byte. Rich-text reader cũng làm y hệt từ offset 1 trên Continue segment đầu tiên. [MS-XLS] 2.5.293 bảo đảm break nằm ở double-byte character boundary khi fHighByte bằng 1, nên không cần bookkeeping cho partial character, nhưng byte arithmetic vẫn là phần bạn phải làm đúng

GetWideStringWithZero thực sự làm gì?

TXLSBlob.GetWideStringWithZero là wide-character reader giữ lại embedded NUL. Hậu tố WithZero đánh dấu việc giữ NUL chứ không đánh dấu độ rộng character: bên trong nó chạy cùng SetString trên PWideChar với Count div SizeOf(WideChar) như GetWideString, chỉ bỏ bước scan terminator. Đối tác one-byte là TXLSBlob.GetStringWithZero, trả về AnsiString. Tên không nói cái nào là cái nào, và ambiguity này đã gây bug thật trong codebase. Misreading cụ thể đáng được gọi tên vì nó trông rất hợp lý: GetWideString cần cch * 2, vậy GetWideStringWithZero chắc là cái nhận cch trực tiếp. Nó thực sự nhận cch mà không phàn nàn, trả về WideString và compiler vui vẻ. Nhưng nó cũng trả về một nửa character, ghép từ cặp byte sai. Đường 8-bit đúng là TXLSBlob.GetString với plain cch byte count, cast sang WideString ở assignment. HotXLS 2.376.0 đã sửa đúng việc dùng sai này trong hai chart decoder

SXViewLink và length gate theo encoding

SXViewLink ($0858, [MS-XLS] 2.4.316) là ví dụ rõ nhất vì nó gói cả hai bất đối xứng vào một header tám byte. Layout là rt(2), unused(2), reserved(2), cch(1), fHigh(1), theo sau bởi body XLUnicodeStringNoCch: fHigh = 1 nghĩa là UTF-16 dài cch * 2 byte, fHigh = 0 nghĩa là character one-byte dài cch byte, còn cch tối đa 255 vì length field chỉ có một byte. HotXLS ghi record vào chart globals trước Units, cạnh PivotChartBits ($0859, [MS-XLS] 2.4.196), khi chart sheet link tới PivotTable view; cách nhìn ở cấp record của machinery đó nằm trong việc ghi BIFF8 PivotTable record trong Delphi

// SXViewLink ([MS-XLS] 2.4.316): rt(2) unused(2) reserved(2) cch(1)
// rồi một XLUnicodeStringNoCch - fHigh(1) theo sau bởi character
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;

Version trước gate cả hai encoding bằng expression wide-character 8 + cch * 2, nên view name 8-bit do Excel ghi sẽ fail guard và decoder trả PivotSourceName rỗng, để IsPivotChart bằng false. Pivot link biến mất khỏi model mà không có một diagnostic nào. Cùng lỗi nằm trong Trendline decoder ($2050, [MS-XLS] 2.4.328), nơi name field theo sau 28 byte numeric payload với cch hai byte ở offset 28, fHigh ở offset 30 và character ở offset 31; trendline caption Excel ghi bằng character 8-bit decode thành string rỗng. Cả hai được sửa trong cùng release. Case 8-bit cũng không phải chuyện cũ chỉ giới hạn ở file Excel 2.0 đến 4.0: Excel hiện tại vẫn ghi BIFF8 payload 8-bit khi mọi character đều vừa một byte

Decode BIFF8 record mới an toàn trong Delphi bằng cách nào?

Ở nơi field thực sự là XLUnicodeString chuẩn, hãy dùng TXLSBlob.GetBiffString thay vì tự viết branch. Nó đọc length field, đọc option byte, dispatch tới reader phù hợp rồi advance cursor qua body. Hai Boolean parameter là phần cần đọc kỹ: is8bit mô tả width của length field chứ không mô tả width của character, còn iswide nói có fHigh option byte theo sau length field hay không. BIFF version dưới $0600 không có cả hai

var
  Offset: LongWord;
begin
  Offset := 6;  // trong SXViewLink, cch byte bắt đầu ở đây
  // is8bit = length field rộng một byte
  // iswide = có fHigh option byte theo sau length field
  Name := Data.GetBiffString(Offset, True, True);
  // Offset giờ trỏ vào byte đầu tiên sau string body

Branch tự viết vẫn có chỗ đứng khi handler phải chịu input truncated hoặc hostile, vì GetBiffString dựa vào việc EnsureReadable raise thay vì bounds check do bạn kiểm soát. Đó là lý do HotXLS chart decoder gate bằng DataLength rồi trả về không có gì thay vì throw: workbook bên thứ ba malformed chỉ nên làm mất một caption chứ không phải cả document. Trade-off có chủ ý, và chính vì vậy encoding-specific guard phải đúng, vì guard là thứ biến bad read thành im lặng

Một phần process cuối cùng, rút ra khá đắt trong cùng release. Hãy assert trên workbook đã save rồi reopen, không phải in-memory model bạn vừa build. Batch 2.376.0 cũng phát hiện một SXEx emitter ([MS-XLS] 2.4.282) khai báo body 24 byte nhưng chỉ ghi 22, làm lệch mọi record sau PivotTable view, kể cả worksheet EOF và mọi chart sheet substream theo sau. Pivot test hiện có không bắt được vì tất cả đều assert trên memory. String decoding có cùng tính chất: chỉ round trip qua file mới thực sự exercise byte count

Nếu bạn làm việc với XLS internals cổ điển trong Delphi hoặc C++Builder và không muốn tự duy trì BIFF8 record reader, các quy tắc encoding trên đã được triển khai và regression-test trong HotXLS Delphi spreadsheet component, đọc và ghi XLS cùng XLSX mà không cần Excel hay OLE automation