Articol tehnic

Decodarea XLUnicodeString BIFF8 în Delphi: cch și fHigh

HotXLS decodează un BIFF8 XLUnicodeString citind mai întâi cch și flag-ul fHigh, apoi alegând reader-ul potrivit encoding-ului: TXLSBlob.GetWideString cu un byte count de cch * 2 când fHigh este 1 și TXLSBlob.GetString când fHigh este 0. Inversează pairing-ul și record-ul produce un string gol sau cu jumătate din lungime, niciodată o excepție

Asta face scumpă această clasă de bug. Un chart se deschide, series-urile se desenează corect, axele sunt bune, iar un trendline caption este pur și simplu gol. Nimic în log, nimic în exception handler, niciun dialog de fișier corupt. Fișierul a fost bun tot timpul; reader-ul a cerut numărul greșit de bytes și a primit exact ce a cerut

De ce revine gol un string BIFF8?

Un string BIFF8 revine gol deoarece un length guard a respins payload-ul înainte să aibă loc read-ul sau deoarece reader-ul s-a oprit la primul NUL găsit. Ambele rute sunt tăcute prin construcție. În HotXLS, guard-ul este de obicei un check explicit de DataLength în record handler și trebuie calculat per encoding: un payload pe 16 bits are nevoie de 8 + cch * 2 bytes pentru un body SXViewLink, dar un payload pe 8 bits are nevoie doar de 8 + cch. Aplică aritmetica wide-character unui record pe 8 bits și fiecare nume scurt eșuează la gate. Comportamentul NUL este a doua capcană, deoarece TXLSBlob.GetString și TXLSBlob.GetWideString scanează ambele rezultatul decodat pentru un terminator și trunchiază acolo, returnând string gol când terminatorul ajunge pe poziția unu. Citește un body pe 16 bits cu jumătate din byte count și păstrezi primele cch div 2 caractere; citește un body pe 8 bits prin wide reader și perechile de bytes formează code points arbitrare. Doar un read prea lung este zgomotos: TXLSBlob.EnsureReadable ridică Blob read exceeds data size când cererea depășește blob-ul. Un read prea scurt nu are o asemenea alarmă

GetWideString numără bytes, nu caractere

TXLSBlob.GetWideString(Index, Count) primește Count în bytes. Intern face un SetString peste un PWideChar cu Count div SizeOf(WideChar), așa că transmiterea unui character count înjumătățește tăcut string-ul. Layout-urile de records BIFF8 exprimă între timp lungimea string-ului în caractere. Fiecare call site pe 16 bits trebuie deci să poarte singur conversia * 2, iar fiecare call site pe 8 bits trebuie să nu o poarte. Aceasta este aceeași graniță de encoding care apare când scrii textul, nu doar când îl citești, și merită parcursă alături de exportul spreadsheet Unicode-safe în Delphi dacă pipeline-ul tău mută string-uri în ambele direcții

// XLUnicodeStringNoCch pe 16 bits: cch caractere, cch * 2 bytes
Name := Data.GetWideString(Start, cch * 2);        // corect
Name := Data.GetWideString(Start, cch);            // jumătate din text, fără eroare

// XLUnicodeStringNoCch pe 8 bits: cch caractere, cch bytes
Name := WideString(Data.GetString(Start, cch));    // corect
Name := Data.GetWideStringWithZero(Start, cch);    // tot un wide reader

Convenția se păstrează oriunde byte stream-ul este parcurs manual. Când HotXLS reunește un String record lung ($0207, [MS-XLS] 2.4.268) din Continue records ($003C), ramura wide calculează segCh din lungimea segmentului și apoi apelează GetWideString(3, segCh * 2), deoarece body-ul record-ului începe la offset 3, iar count-ul este tot în bytes. Rich-text reader-ul face același lucru de la offset 1 pe primul Continue segment. [MS-XLS] 2.5.293 garantează că break-ul cade pe o limită de caracter double-byte când fHighByte este 1, așa că nu este nevoie de bookkeeping pentru un caracter parțial, dar aritmetica bytes rămâne responsabilitatea ta

Ce face în realitate GetWideStringWithZero?

TXLSBlob.GetWideStringWithZero este un wide-character reader care păstrează NUL-urile embedded. Sufixul WithZero marchează păstrarea NUL-ului, nu lățimea caracterului: intern rulează același SetString peste un PWideChar cu Count div SizeOf(WideChar) ca GetWideString, doar fără terminator scan. Omologul pe un byte este TXLSBlob.GetStringWithZero, care returnează un AnsiString. Numele nu spune care este care, iar această ambiguitate a produs bug-uri reale în codebase. Misreading-ul concret merită numit, pentru că pare plauzibil: GetWideString are nevoie de cch * 2, deci GetWideStringWithZero trebuie să fie cel care primește cch direct. Primește într-adevăr cch fără plângere, returnează un WideString, iar compiler-ul este mulțumit. Returnează și jumătate din caractere, asamblate din perechi de bytes greșite. Ruta corectă pe 8 bits este TXLSBlob.GetString cu un plain cch byte count, cast la WideString la assignment. HotXLS 2.376.0 a reparat exact această folosire greșită în doi chart decoders

SXViewLink și length gate-ul per encoding

SXViewLink ($0858, [MS-XLS] 2.4.316) este exemplul lucrat cel mai clar, deoarece împachetează ambele asimetrii într-un header de opt bytes. Layout-ul este rt(2), unused(2), reserved(2), cch(1), fHigh(1), urmat de un body XLUnicodeStringNoCch: fHigh = 1 înseamnă cch * 2 bytes de UTF-16, fHigh = 0 înseamnă cch bytes de caractere pe un byte, iar cch este limitat la 255 deoarece length field-ul este un singur byte. HotXLS scrie record-ul în chart globals înainte de Units, lângă PivotChartBits ($0859, [MS-XLS] 2.4.196), când un chart sheet se leagă la un PivotTable view; perspectiva la nivel de record a acestei machinery este acoperită în scrierea BIFF8 PivotTable records în Delphi

// SXViewLink ([MS-XLS] 2.4.316): rt(2) unused(2) reserved(2) cch(1)
// apoi un XLUnicodeStringNoCch - fHigh(1) urmat de caractere
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;

Cele două branches nu sunt cosmetice. O versiune anterioară punea ambele encodings în spatele expresiei wide-character 8 + cch * 2, așa că un view name pe 8 bits scris de Excel eșua la guard, iar decoder-ul returna un PivotSourceName gol cu IsPivotChart lăsat false. Pivot link-ul dispărea din model fără un singur diagnostic. Aceeași greșeală exista în Trendline decoder ($2050, [MS-XLS] 2.4.328), unde name field-ul urmează 28 bytes de payload numeric, cu un cch pe doi bytes la offset 28, fHigh la offset 30 și caracterele la offset 31; trendline captions pe care Excel le scrisese cu caractere pe 8 bits se decodau ca string-uri goale. Ambele au fost reparate în aceeași versiune. Iar cazul pe 8 bits nu este o curiozitate legacy limitată la fișiere Excel 2.0 până la 4.0: Excel curent încă scrie payload-uri BIFF8 pe 8 bits ori de câte ori fiecare caracter încape într-un byte

Cum decodezi în siguranță un record BIFF8 nou în Delphi?

Când field-ul este într-adevăr un XLUnicodeString standard, folosește TXLSBlob.GetBiffString în loc să scrii manual branch-ul. Citește length field-ul, citește option byte-ul, face dispatch către reader-ul potrivit și avansează cursorul peste body. Cele două parameters booleene sunt partea care trebuie citită atent: is8bit descrie lățimea length field-ului, nu lățimea caracterelor, iar iswide spune dacă există vreun option byte fHigh după length field. Versiunile BIFF sub $0600 nu au niciuna

var
  Offset: LongWord;
begin
  Offset := 6;  // în SXViewLink, byte-ul cch începe aici
  // is8bit = length field-ul are lățimea de un byte
  // iswide = urmează un option byte fHigh după length field
  Name := Data.GetBiffString(Offset, True, True);
  // Offset indică acum primul byte de după body-ul string-ului

Branches scrise manual își au încă locul când handler-ul trebuie să reziste la input trunchiat sau hostile, deoarece GetBiffString se bazează pe ridicarea din EnsureReadable, nu pe un bounds check pe care îl controlezi tu. De aceea decoder-ul chart-ului HotXLS verifică DataLength și nu returnează nimic în loc să arunce: un workbook malformed de la un terț ar trebui să te coste un caption, nu întregul document. Compromisul este intenționat și tocmai de aceea guard-ul specific encoding-ului trebuie să fie corect, deoarece el este lucrul care transformă un read greșit în tăcere

Încă o piesă de proces, învățată greu în aceeași versiune. Fă aserțiuni pe un workbook salvat și redeschis, nu pe modelul in-memory pe care tocmai l-ai construit. Batch-ul 2.376.0 a scos la iveală și un emitter SXEx ([MS-XLS] 2.4.282) care declara un body de 24 bytes și scria doar 22, desincronizând fiecare record după PivotTable view, inclusiv worksheet EOF și orice chart sheet substream care urma. Testele de pivot existente nu l-au prins deoarece toate verificau memoria. Decodarea string-urilor are aceeași proprietate: doar un round trip prin fișier exercită efectiv byte counts

Dacă lucrezi cu internals XLS clasice în Delphi sau C++Builder și preferi să nu întreții propriul BIFF8 record reader, regulile de encoding de mai sus sunt deja implementate și acoperite de regression tests în componenta spreadsheet HotXLS pentru Delphi, care citește și scrie XLS și XLSX fără Excel sau orice OLE automation