HotXLS afkoder en BIFF8 XLUnicodeString ved først at læse cch- og fHigh-flaget og derefter vælge den reader, der passer til encodingen: TXLSBlob.GetWideString med et byteantal på cch * 2, når fHigh er 1, og TXLSBlob.GetString, når fHigh er 0. Par dem på den anden måde, og recorden giver en tom eller halvlang string, aldrig en exception
Det er det, der gør denne type bug dyr. Et chart åbner, serierne tegnes korrekt, akserne er rigtige, og én trendline-caption er bare blank. Intet i loggen, intet i exception-handleren, ingen dialog om en korrupt fil. Filen var fin hele tiden; readeren bad om det forkerte antal bytes og fik præcis det, den bad om
Hvorfor kommer en BIFF8-string tom tilbage?
En BIFF8-string kommer tom tilbage, fordi en længdeguard afviste payloaden, før læsningen overhovedet skete, eller fordi readeren stoppede ved den første NUL, den fandt. Begge veje er lydløse af konstruktion. I HotXLS er guarden normalt en eksplicit DataLength-kontrol i record-handleren, og den skal beregnes pr. encoding: En 16-bit-payload kræver 8 + cch * 2 bytes til en SXViewLink-body, mens en 8-bit-payload kun kræver 8 + cch. Anvend den brede tegn-aritmetik på en 8-bit-record, og hvert kort navn fejler kontrollen. NUL-adfærden er den anden fælde, fordi TXLSBlob.GetString og TXLSBlob.GetWideString begge scanner det afkodede resultat efter en terminator og trunkerer dér, og returnerer en tom string, når terminatoren lander i position et. Læs en 16-bit-body med halvt byteantal, og du beholder de første cch div 2 tegn; læs en 8-bit-body gennem den brede reader, og byteparrene danner vilkårlige code points. Kun en for lang læsning larmer: TXLSBlob.EnsureReadable rejser Blob read exceeds data size, når anmodningen går forbi blobben. Under-reading har ingen sådan alarm
GetWideString tæller bytes, ikke tegn
TXLSBlob.GetWideString(Index, Count) tager Count i bytes. Internt udfører den et SetString over en PWideChar med Count div SizeOf(WideChar), så hvis du sender et tegnantal, halverer den lydløst strengen. BIFF8-recordlayoutet udtrykker imidlertid string-længde i tegn. Hvert 16-bit-kald skal derfor selv bære konverteringen * 2, og hvert 8-bit-kald skal undlade den. Det er den samme encoding-grænse, der dukker op, når du skriver tekst ud igen i stedet for at læse den, og den er værd at læse sammen med Unicode-sikker regnearkseksport i Delphi, hvis din pipeline flytter strings i begge retninger
// 16-bit XLUnicodeStringNoCch: cch tegn, cch * 2 bytes
Name := Data.GetWideString(Start, cch * 2); // korrekt
Name := Data.GetWideString(Start, cch); // halvdelen af teksten, ingen fejl
// 8-bit XLUnicodeStringNoCch: cch tegn, cch bytes
Name := WideString(Data.GetString(Start, cch)); // korrekt
Name := Data.GetWideStringWithZero(Start, cch); // stadig en wide reader
Konventionen gælder alle steder, hvor byte-streamen gennemgås i hånden. Når HotXLS samler en lang String-record ($0207, [MS-XLS] 2.4.268) igen fra sine Continue-records ($003C), beregner den brede gren segCh ud fra segmentlængden og kalder derefter GetWideString(3, segCh * 2), fordi record-bodyen starter ved offset 3, og count stadig er bytes. Rich-text-readeren gør det samme fra offset 1 på det første Continue-segment. [MS-XLS] 2.5.293 garanterer, at bruddet ligger på en dobbeltbyte-tegngrænse, når fHighByte er 1, så der er ikke brug for bogføring af delvise tegn, men bytearitmetikken er stadig din at få rigtig
Hvad gør GetWideStringWithZero faktisk?
TXLSBlob.GetWideStringWithZero er en wide-character-reader, der bevarer indlejrede NULer. Suffikset WithZero markerer NUL-bevarelse og ikke tegnbredde: Internt kører den det samme SetString over en PWideChar med Count div SizeOf(WideChar) som GetWideString, blot uden terminatorscanningen. Modstykket til én byte er TXLSBlob.GetStringWithZero, som returnerer en AnsiString. Intet i navnet siger, hvilken der er hvilken, og den tvetydighed har kostet denne kodebase reelle bugs. Den konkrete fejllæsning er værd at navngive, fordi den ser så plausibel ud: GetWideString kræver cch * 2, så GetWideStringWithZero må være den, der tager cch direkte. Den tager ganske vist cch uden klage, den returnerer en WideString, og compileren er tilfreds. Den returnerer også halvdelen af tegnene, samlet fra de forkerte bytepar. Den korrekte 8-bit-vej er TXLSBlob.GetString med et almindeligt cch-byteantal, castet til WideString ved assignment. HotXLS 2.376.0 rettede netop denne misbrug i to chart-decodere
SXViewLink og den per-encoding længdeguard
SXViewLink ($0858, [MS-XLS] 2.4.316) er det reneste gennemarbejdede eksempel, fordi den pakker begge asymmetrier ind i én header på otte bytes. Layoutet er rt(2), unused(2), reserved(2), cch(1), fHigh(1), efterfulgt af en XLUnicodeStringNoCch-body: fHigh = 1 betyder cch * 2 bytes UTF-16, fHigh = 0 betyder cch bytes med etbyte-tegn, og cch er begrænset til 255, fordi længdefeltet er én byte. HotXLS skriver recorden i chart-globals før Units, ved siden af PivotChartBits ($0859, [MS-XLS] 2.4.196), når et chartsheet linker til en PivotTable-view; record-niveauet for denne mekanik dækkes i skrivning af BIFF8 PivotTable-records i Delphi
// SXViewLink ([MS-XLS] 2.4.316): rt(2) unused(2) reserved(2) cch(1)
// derefter en XLUnicodeStringNoCch - fHigh(1) efterfulgt af tegnene
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 to grene er ikke kosmetik. En tidligere version holdt begge encodings bag det brede tegnudtryk 8 + cch * 2, så et Excel-skrevet 8-bit-viewnavn fejlede guarden, og decoderen returnerede et tomt PivotSourceName med IsPivotChart stående false. Pivot-linket forsvandt fra modellen uden en eneste diagnosticering. Den identiske fejl lå i Trendline-decoderens ($2050, [MS-XLS] 2.4.328), hvor navnefeltet følger efter 28 bytes numerisk payload med et tobytes cch ved offset 28, fHigh ved offset 30 og tegnene ved offset 31; trendline-captions, som Excel havde skrevet med 8-bit-tegn, blev afkodet som tomme strings. Begge blev rettet i samme release. Og 8-bit-tilfældet er ikke en historisk kuriositet, der er begrænset til Excel 2.0 til 4.0-filer: Aktuelt Excel skriver stadig 8-bit BIFF8-payloads, når hvert tegn kan være i én byte
Hvordan afkoder man sikkert en ny BIFF8-record i Delphi?
Hvis feltet virkelig er en standard XLUnicodeString, så brug TXLSBlob.GetBiffString i stedet for at skrive grenen selv. Den læser længdefeltet, læser option-byten, sender videre til den passende reader og flytter cursoren forbi bodyen. De to Boolean-parametre er den del, der kræver opmærksomhed: is8bit beskriver bredden af længdefeltet og ikke bredden af tegnene, mens iswide siger, om der overhovedet findes en fHigh-option-byte. BIFF-versioner under $0600 har ingen af delene
var
Offset: LongWord;
begin
Offset := 6; // i SXViewLink starter cch-byten her
// is8bit = længdefeltet er én byte bredt
// iswide = en fHigh-option-byte følger efter længdefeltet
Name := Data.GetBiffString(Offset, True, True);
// Offset peger nu på den første byte efter string-bodyen
Håndskrevne grene fortjener stadig deres plads, når handleren skal overleve afkortet eller fjendtligt input, fordi GetBiffString læner sig op ad, at EnsureReadable rejser en fejl, i stedet for en bounds-kontrol, du selv styrer. Derfor kontrollerer HotXLS-chart-decoderen DataLength og returnerer ingenting i stedet for at kaste: En fejlformet workbook fra tredjepart bør koste dig én caption og ikke hele dokumentet. Afvejningen er bevidst, og det er netop derfor, at den encoding-specifikke guard skal være rigtig, eftersom guarden er det, der omdanner en dårlig læsning til tavshed
Én sidste procesdetalje, lært på den hårde måde i samme release. Assert på en gemt og genåbnet workbook og ikke på den in-memory-model, du lige har bygget. 2.376.0-batchens arbejde fandt også en SXEx-emitter ([MS-XLS] 2.4.282), der deklarerede en body på 24 bytes, men kun skrev 22, hvilket fejljusterede hver record efter PivotTable-viewet, inklusive worksheet-EOF og enhver chart-sheet-substream, der fulgte. De eksisterende pivot-tests fangede det aldrig, fordi de alle hævdede noget om memory. String-afkodning har samme egenskab: En round-trip gennem filen er den eneste test, der rent faktisk udøver byteantallene
Hvis du arbejder med klassiske XLS-internals i Delphi eller C++Builder og helst ikke selv vil vedligeholde en BIFF8-record-reader, er encodingreglerne ovenfor allerede implementeret og regressionstestet i HotXLS Delphi-regnearkskomponenten, som læser og skriver XLS og XLSX uden Excel eller nogen OLE-automation