HotXLS decodeert een BIFF8 XLUnicodeString door eerst cch en de flag fHigh te lezen en daarna de reader te kiezen die bij de encoding past: TXLSBlob.GetWideString met een bytecount van cch * 2 wanneer fHigh 1 is, en TXLSBlob.GetString wanneer fHigh 0 is. Koppel je ze andersom, dan levert het record een lege of half zo lange string op en nooit een exception
Dat maakt deze bugklasse zo duur. Een chart opent, de series tekenen correct, de assen kloppen en één trendline-caption is simpelweg leeg. Niets in de log, niets in de exception handler en geen corrupt-file-dialog. Het bestand was de hele tijd goed; de reader vroeg om het verkeerde aantal bytes en kreeg precies wat hij vroeg
Waarom komt een BIFF8-string leeg terug?
Een BIFF8-string komt leeg terug omdat een lengteguard de payload afwees voordat de read ooit plaatsvond, of omdat de reader stopte bij de eerste NUL die hij vond. Beide paden zijn by construction stil. In HotXLS is de guard meestal een expliciete DataLength-check in de recordhandler, en die moet per encoding worden berekend: een 16-bit-payload heeft voor een SXViewLink-body 8 + cch * 2 bytes nodig, maar een 8-bit-payload slechts 8 + cch. Pas je de wide-character arithmetic toe op een 8-bit-record, dan faalt elke korte naam voor de gate. Het NUL-gedrag is de tweede valkuil, omdat TXLSBlob.GetString en TXLSBlob.GetWideString allebei het gedecodeerde resultaat op een terminator scannen en daar afkappen, waarbij ze een lege string teruggeven wanneer de terminator op positie één staat. Lees je een 16-bit-body met de helft van de bytecount, dan houd je de eerste cch div 2 tekens over; lees je een 8-bit-body via de wide reader, dan vormen de byteparen willekeurige codepunten. Alleen een te lange read is luid: TXLSBlob.EnsureReadable werpt Blob read exceeds data size wanneer de request voorbij de blob loopt. Te weinig lezen heeft geen dergelijk alarm
GetWideString telt bytes en geen tekens
TXLSBlob.GetWideString(Index, Count) neemt Count in bytes aan. Intern doet hij een SetString over een PWideChar met Count div SizeOf(WideChar), dus een character count meegeven halveert stilletjes de string. De BIFF8-recordlayouts drukken de stringlengte ondertussen uit in tekens. Elke 16-bit-callsite moet de conversie * 2 dus zelf dragen, en elke 8-bit-callsite juist niet. Dit is dezelfde encodinggrens die zichtbaar wordt wanneer je tekst opnieuw wegschrijft in plaats van hem te lezen, dus lees dit samen met Unicodeveilige spreadsheetexport in Delphi als je pipeline strings in beide richtingen verplaatst
// 16-bit XLUnicodeStringNoCch: cch tekens, cch * 2 bytes
Name := Data.GetWideString(Start, cch * 2); // correct
Name := Data.GetWideString(Start, cch); // halve tekst, geen fout
// 8-bit XLUnicodeStringNoCch: cch tekens, cch bytes
Name := WideString(Data.GetString(Start, cch)); // correct
Name := Data.GetWideStringWithZero(Start, cch); // nog steeds een wide reader
De conventie geldt overal waar de bytestream met de hand wordt doorlopen. Wanneer HotXLS een lang String-record ($0207, [MS-XLS] 2.4.268) uit zijn Continue-records ($003C) weer samenstelt, berekent de wide branch segCh uit de segmentlengte en roept hij daarna GetWideString(3, segCh * 2) aan, omdat de recordbody op offset 3 begint en de count nog steeds bytes is. De rich-text-reader doet hetzelfde vanaf offset 1 op het eerste Continue-segment. [MS-XLS] 2.5.293 garandeert dat de breuk op een double-byte-charactergrens ligt wanneer fHighByte 1 is, dus gedeeltelijke-characteradministratie is niet nodig, maar de byte arithmetic moet je nog steeds goed krijgen
Wat doet GetWideStringWithZero precies?
TXLSBlob.GetWideStringWithZero is een wide-character-reader die ingebedde NULs behoudt. Het suffix WithZero markeert NUL-retentie en niet de character width: intern draait hij dezelfde SetString over een PWideChar met Count div SizeOf(WideChar) als GetWideString, maar zonder terminatorscan. De één-byte-tegenhanger is TXLSBlob.GetStringWithZero, die een AnsiString retourneert. Niets in de naam zegt welke welke is, en die ambiguïteit heeft deze codebase echte bugs gekost. De specifieke mislezing is het benoemen waard omdat ze zo plausibel klinkt: GetWideString heeft cch * 2 nodig, dus GetWideStringWithZero moet degene zijn die cch rechtstreeks neemt. Dat doet hij cch zonder bezwaar, hij retourneert een WideString en de compiler is tevreden. Hij retourneert ook de helft van de tekens, samengesteld uit de verkeerde byteparen. Het correcte 8-bit-pad is TXLSBlob.GetString met een gewone cch-bytecount, bij de assignment naar WideString gecast. HotXLS 2.376.0 repareerde precies dat misbruik in twee chartdecoders
SXViewLink en de lengteguard per encoding
SXViewLink ($0858, [MS-XLS] 2.4.316) is het duidelijkste uitgewerkte voorbeeld, omdat het beide asymmetrieën in één header van acht bytes verpakt. De layout is rt(2), unused(2), reserved(2), cch(1), fHigh(1), gevolgd door een XLUnicodeStringNoCch-body: fHigh = 1 betekent cch * 2 bytes UTF-16, fHigh = 0 betekent cch bytes één-byte-tekens en cch is begrensd op 255 omdat het lengteveld één byte groot is. HotXLS schrijft het record in de chartglobals vóór Units, naast PivotChartBits ($0859, [MS-XLS] 2.4.196), wanneer een chartsheet aan een PivotTable-view is gekoppeld; het recordniveau van die machinery wordt behandeld bij BIFF8-PivotTable-records schrijven in Delphi
// SXViewLink ([MS-XLS] 2.4.316): rt(2) unused(2) reserved(2) cch(1)
// daarna een XLUnicodeStringNoCch - fHigh(1) gevolgd door de tekens
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;
De twee branches zijn niet cosmetisch. Een eerdere versie gate'te beide encodings achter de wide-character-expressie 8 + cch * 2, waardoor een door Excel geschreven 8-bit-viewnaam de guard niet haalde en de decoder een lege PivotSourceName retourneerde met IsPivotChart op false. De pivotlink verdween uit het model zonder ook maar één diagnose. Dezelfde fout zat in de Trendline-decoder ($2050, [MS-XLS] 2.4.328), waar het naamveld volgt op 28 bytes numerieke payload met een 2-byte cch op offset 28, fHigh op offset 30 en de tekens op offset 31; trendlinecaptions die Excel met 8-bit-tekens had geschreven, decodeerden als lege strings. Beide zijn in dezelfde release gerepareerd. En het 8-bit-geval is geen legacycuriositeit die beperkt is tot Excel 2.0 tot en met 4.0-bestanden: huidige Excel schrijft nog steeds 8-bit-BIFF8-payloads wanneer elk teken in één byte past
Hoe decodeer je een nieuw BIFF8-record veilig in Delphi?
Wanneer het veld echt een standaard XLUnicodeString is, gebruik dan TXLSBlob.GetBiffString in plaats van de branch met de hand te bouwen. Die leest het lengteveld, leest de option byte, dispatcht naar de passende reader en schuift de cursor voorbij de body. De twee Booleaanse parameters zijn het deel dat je zorgvuldig moet lezen: is8bit beschrijft de breedte van het lengteveld en niet de breedte van de tekens, en iswide zegt of er überhaupt een fHigh-optionbyte aanwezig is. BIFF-versies onder $0600 hebben geen van beide
var
Offset: LongWord;
begin
Offset := 6; // in SXViewLink begint de cch-byte hier
// is8bit = het lengteveld is één byte breed
// iswide = een fHigh-optionbyte volgt op het lengteveld
Name := Data.GetBiffString(Offset, True, True);
// Offset wijst nu naar de eerste byte na de stringbody
Met de hand gebouwde branches blijven hun plaats houden wanneer de handler truncated of hostile input moet overleven, omdat GetBiffString leunt op een exception uit EnsureReadable en niet op een bounds check die je zelf beheert. Daarom gate't de HotXLS-chartdecoder op DataLength en retourneert hij niets in plaats van te werpen: een malformed workbook van een derde partij moet je één caption kosten en niet het hele document. Die trade-off is bewust, en precies daarom moet de encoding-specifieke guard kloppen, want de guard is wat een verkeerde read in stilte omzet
Nog één procesdetail, op de harde manier geleerd in dezelfde release. Assert op een opgeslagen en opnieuw geopend workbook en niet op het in-memorymodel dat je net hebt gebouwd. De batch van 2.376.0 bracht ook een SXEx-emitter aan het licht ([MS-XLS] 2.4.282) die een body van 24 bytes declareerde en er maar 22 schreef, waardoor elk record na de PivotTable-view verkeerd uitgelijnd raakte, inclusief de worksheet-EOF en elke chartsheet-substream die volgde. De bestaande pivottests vingen dit nooit omdat ze allemaal tegen memory asserteten. Voor stringdecoding geldt hetzelfde: een round-trip door het bestand is de enige test die de bytecounts daadwerkelijk uitvoert
Als je met klassieke XLS-internals werkt in Delphi of C++Builder en liever geen eigen BIFF8-recordreader onderhoudt, zijn de bovenstaande encodingregels al geïmplementeerd en met regressietests afgedekt in de HotXLS Delphi spreadsheet component, die XLS en XLSX leest en schrijft zonder Excel of OLE-automation