Articolo tecnico

Decodifica XLUnicodeString BIFF8 in Delphi: cch e fHigh

HotXLS decodifica una XLUnicodeString BIFF8 leggendo prima cch e il flag fHigh, poi scegliendo il reader coerente con la codifica: TXLSBlob.GetWideString con un conteggio di byte pari a cch * 2 quando fHigh vale 1, e TXLSBlob.GetString quando fHigh vale 0. Abbinateli al contrario e il record produce una stringa vuota o di metà lunghezza, mai un'eccezione

È questo a rendere costosa la classe di bug. Un chart si apre, le serie vengono disegnate correttamente, gli assi sono giusti e una caption di trendline è semplicemente vuota. Nulla nel log, nulla nell'exception handler, nessun dialog di file corrotto. Il file era corretto per tutto il tempo; il reader ha chiesto il numero sbagliato di byte e ha ottenuto esattamente ciò che aveva chiesto

Perché una stringa BIFF8 torna vuota?

Una stringa BIFF8 torna vuota perché una length guard ha rifiutato il payload prima ancora della lettura, oppure perché il reader si è fermato al primo NUL incontrato. Entrambi i percorsi sono silenziosi per costruzione. In HotXLS la guardia è di solito un controllo esplicito di DataLength nel record handler e deve essere calcolata per codifica: un payload a 16 bit richiede 8 + cch * 2 byte per un body SXViewLink, mentre uno a 8 bit ne richiede 8 + cch. Applicate l'aritmetica wide a un record a 8 bit e ogni nome corto fallisce il gate. Il secondo tranello è il comportamento sul NUL, perché TXLSBlob.GetString e TXLSBlob.GetWideString cercano entrambi un terminatore nel risultato decodificato e troncano lì, restituendo una stringa vuota quando il terminatore cade in posizione uno. Leggete un body a 16 bit con metà dei byte e mantenete i primi cch div 2 caratteri; leggete un body a 8 bit attraverso il reader wide e le coppie di byte formano code point arbitrari. Solo una lettura troppo lunga è rumorosa: TXLSBlob.EnsureReadable genera Blob read exceeds data size quando la richiesta supera la dimensione del blob. Una lettura corta non ha alcun allarme simile

GetWideString conta byte, non caratteri

TXLSBlob.GetWideString(Index, Count) riceve Count in byte. Internamente esegue un SetString su un PWideChar con Count div SizeOf(WideChar), quindi passare un conteggio di caratteri dimezza silenziosamente la stringa. I layout dei record BIFF8 esprimono invece la lunghezza in caratteri. Ogni call a 16 bit deve quindi portare da sola la conversione * 2, e ogni call a 8 bit deve evitarla. È lo stesso confine di codifica che emerge quando si riscrive testo invece di leggerlo, cosa che vale la pena leggere insieme a l'export spreadsheet Unicode-safe in Delphi se la pipeline sposta stringhe in entrambe le direzioni

// XLUnicodeStringNoCch a 16 bit: cch caratteri, cch * 2 byte
Name := Data.GetWideString(Start, cch * 2);        // corretto
Name := Data.GetWideString(Start, cch);            // metà del testo, nessun errore

// XLUnicodeStringNoCch a 8 bit: cch caratteri, cch byte
Name := WideString(Data.GetString(Start, cch));    // corretto
Name := Data.GetWideStringWithZero(Start, cch);    // resta un reader wide

La convenzione vale ovunque il byte stream venga percorso a mano. Quando HotXLS ricompone un lungo String record ($0207, [MS-XLS] 2.4.268) dai propri Continue record ($003C), il ramo wide calcola segCh dalla lunghezza del segmento e poi chiama GetWideString(3, segCh * 2), perché il body del record parte all'offset 3 e il conteggio resta in byte. Il reader del rich text fa la stessa cosa dall'offset 1 sul primo segmento Continue. [MS-XLS] 2.5.293 garantisce che la rottura cada su un confine di carattere double-byte quando fHighByte vale 1, quindi non serve gestire caratteri parziali, ma l'aritmetica dei byte resta a vostro carico

Cosa fa davvero GetWideStringWithZero?

TXLSBlob.GetWideStringWithZero è un reader wide che conserva i NUL embedded. Il suffisso WithZero indica la conservazione dei NUL, non la larghezza dei caratteri: internamente esegue lo stesso SetString su un PWideChar con Count div SizeOf(WideChar) di GetWideString, meno la scansione del terminatore. Il corrispondente a un byte è TXLSBlob.GetStringWithZero, che restituisce una AnsiString. Il nome non dice quale dei due sia quale, e questa ambiguità ha causato bug reali in questo codebase. Vale la pena nominare l'errore specifico, perché sembra plausibile: GetWideString richiede cch * 2, quindi GetWideStringWithZero deve essere quello che riceve direttamente cch. Accetta davvero cch senza protestare, restituisce davvero una WideString e il compilatore è contento. Restituisce anche metà dei caratteri, assemblati da coppie di byte sbagliate. Il percorso corretto a 8 bit è TXLSBlob.GetString con un semplice conteggio di byte cch, convertito a WideString nell'assegnazione. HotXLS 2.376.0 ha corretto esattamente questo abuso in due chart decoder

SXViewLink e il gate della lunghezza per codifica

SXViewLink ($0858, [MS-XLS] 2.4.316) è l'esempio più pulito, perché racchiude entrambe le asimmetrie in un header da otto byte. Il layout è rt(2), unused(2), reserved(2), cch(1), fHigh(1), seguito da un body XLUnicodeStringNoCch: fHigh = 1 significa cch * 2 byte di UTF-16, fHigh = 0 significa cch byte di caratteri a un byte e cch è limitato a 255 perché il campo length è un singolo byte. HotXLS scrive il record nei chart globals prima di Units, accanto a PivotChartBits ($0859, [MS-XLS] 2.4.196), quando un chart sheet è collegato a una PivotTable view; la vista a livello record di quel meccanismo è trattata in scrivere record PivotTable BIFF8 in Delphi

// SXViewLink ([MS-XLS] 2.4.316): rt(2) unused(2) reserved(2) cch(1)
// poi un XLUnicodeStringNoCch - fHigh(1) seguito dai caratteri
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;

I due rami non sono cosmetici. Una versione precedente applicava a entrambe le codifiche l'espressione wide 8 + cch * 2, così un view name a 8 bit scritto da Excel falliva la guardia e il decoder restituiva un PivotSourceName vuoto con IsPivotChart lasciato a false. Il pivot link spariva dal modello senza una sola diagnosi. Lo stesso errore era presente nel decoder Trendline ($2050, [MS-XLS] 2.4.328), dove il name field segue 28 byte di payload numerico con un cch a due byte all'offset 28, fHigh all'offset 30 e i caratteri all'offset 31; le trendline caption che Excel aveva scritto con caratteri a 8 bit venivano decodificate come stringhe vuote. Entrambi sono stati corretti nella stessa release. Inoltre il caso a 8 bit non è una curiosità legacy confinata ai file Excel da 2.0 a 4.0: Excel attuale scrive ancora payload BIFF8 a 8 bit quando ogni carattere entra in un byte

Come si decodifica in sicurezza un nuovo record BIFF8 in Delphi?

Quando il campo è davvero una XLUnicodeString standard, usate TXLSBlob.GetBiffString invece di riscrivere a mano il ramo. Legge il length field, legge l'option byte, seleziona il reader corrispondente e avanza il cursore oltre il body. I due parametri Boolean sono la parte da leggere con attenzione: is8bit descrive la larghezza del campo length, non quella dei caratteri, e iswide dice se dopo il campo length è presente un option byte fHigh. Le versioni BIFF inferiori a $0600 non hanno nessuno dei due

var
  Offset: LongWord;
begin
  Offset := 6;  // in SXViewLink il byte cch inizia qui
  // is8bit = il campo length è largo un byte
  // iswide = dopo il campo length segue un option byte fHigh
  Name := Data.GetBiffString(Offset, True, True);
  // Offset ora punta al primo byte dopo il body della stringa

I rami scritti a mano mantengono il proprio valore quando l'handler deve sopravvivere a input troncato o ostile, perché GetBiffString si affida a EnsureReadable che genera un errore invece di un controllo dei bounds sotto il vostro controllo. Per questo il chart decoder HotXLS controlla DataLength e restituisce nulla invece di generare un'eccezione: un workbook di terze parti malformato dovrebbe costare una caption, non l'intero documento. Il compromesso è deliberato, ed è proprio per questo che il gate specifico per codifica deve essere corretto, perché è il gate a trasformare una lettura errata in silenzio

Un ultimo aspetto di processo, imparato a caro prezzo nella stessa release. Fate le asserzioni su un workbook salvato e riaperto, non sul modello in memoria appena costruito. Il batch 2.376.0 ha trovato anche un emitter SXEx ([MS-XLS] 2.4.282) che dichiarava un body da 24 byte e ne scriveva solo 22, disallineando ogni record dopo la PivotTable view, compreso il worksheet EOF e qualsiasi chart sheet substream successivo. I test pivot esistenti non l'avevano rilevato perché verificavano tutti la memoria. La decodifica delle stringhe ha la stessa proprietà: solo un round-trip attraverso il file esercita davvero i conteggi dei byte

Se lavorate con gli internals XLS classici in Delphi o C++Builder e preferite non mantenere un vostro record reader BIFF8, le regole di codifica sopra sono già implementate e coperte da regressioni nel componente spreadsheet Delphi HotXLS, che legge e scrive XLS e XLSX senza Excel né automazione OLE