Technischer Artikel

BIFF8-XLUnicodeString in Delphi: cch und fHigh

HotXLS dekodiert einen BIFF8-XLUnicodeString, indem es zuerst cch und das Flag fHigh liest und dann den zur Kodierung passenden Leser wählt: TXLSBlob.GetWideString mit einer Bytezahl von cch * 2, wenn fHigh 1 ist, und TXLSBlob.GetString, wenn fHigh 0 ist. Vertauscht man beide, liefert der Record einen leeren oder halb so langen String und niemals eine Exception

Das macht diese Fehlerklasse so teuer. Ein Chart öffnet, Serien werden korrekt gezeichnet, die Achsen stimmen, und eine Beschriftung der Trendline ist einfach leer. Nichts im Log, nichts im Exception-Handler, kein Dialog über eine beschädigte Datei. Die Datei war die ganze Zeit in Ordnung; der Reader fragte nach der falschen Bytezahl und bekam exakt, was er verlangt hatte

Warum kommt ein BIFF8-String leer zurück?

Ein BIFF8-String kommt leer zurück, weil ein Längenschutz den Payload vor dem eigentlichen Lesen abgewiesen hat oder weil der Reader beim ersten gefundenen NUL anhielt. Beide Wege sind konstruktionsbedingt still. In HotXLS ist die Prüfung meist eine ausdrückliche DataLength-Kontrolle im Recordhandler, und sie muss je Kodierung berechnet werden: Ein 16-Bit-Payload benötigt für einen SXViewLink-Body 8 + cch * 2 Bytes, ein 8-Bit-Payload nur 8 + cch. Wendet man die Wide-Character-Arithmetik auf einen 8-Bit-Record an, scheitert jeder kurze Name am Gate. Die NUL-Behandlung ist die zweite Falle, denn TXLSBlob.GetString und TXLSBlob.GetWideString suchen beide im dekodierten Ergebnis nach einem Terminator und schneiden dort ab; landet der Terminator an Position eins, kommt ein leerer String zurück. Liest man einen 16-Bit-Body mit der halben Bytezahl, bleiben die ersten cch div 2 Zeichen. Liest man einen 8-Bit-Body über den Wide-Reader, bilden die Bytepaare beliebige Codepoints. Nur ein zu langes Lesen ist laut: TXLSBlob.EnsureReadable löst Blob read exceeds data size aus, wenn die Anfrage über den Blob hinausläuft. Zu wenig zu lesen hat keinen solchen Alarm

GetWideString zählt Bytes und keine Zeichen

TXLSBlob.GetWideString(Index, Count) nimmt Count in Bytes entgegen. Intern führt es ein SetString über einen PWideChar mit Count div SizeOf(WideChar) aus, daher halbiert die Übergabe einer Zeichenzahl den String still. Die BIFF8-Recordlayouts geben Stringlängen dagegen in Zeichen an. Jeder 16-Bit-Aufruf muss die Umrechnung * 2 selbst tragen, und jeder 8-Bit-Aufruf darf sie nicht tragen. Dieselbe Kodierungsgrenze erscheint beim erneuten Schreiben von Text, daher lohnt sich bei Pipelines, die Strings in beide Richtungen bewegen, die Lektüre zusammen mit Unicode-sicherem Spreadsheet-Export in Delphi

// 16-Bit-XLUnicodeStringNoCch: cch Zeichen, cch * 2 Bytes
Name := Data.GetWideString(Start, cch * 2);        // korrekt
Name := Data.GetWideString(Start, cch);            // halber Text, kein Fehler

// 8-Bit-XLUnicodeStringNoCch: cch Zeichen, cch Bytes
Name := WideString(Data.GetString(Start, cch));    // korrekt
Name := Data.GetWideStringWithZero(Start, cch);    // weiterhin ein Wide-Reader

Die Konvention gilt überall, wo der Byte-Stream von Hand durchlaufen wird. Wenn HotXLS einen langen String-Record ($0207, [MS-XLS] 2.4.268) aus seinen Continue-Records ($003C) wieder zusammensetzt, berechnet der Wide-Zweig segCh aus der Segmentlänge und ruft anschließend GetWideString(3, segCh * 2) auf, weil der Record-Body bei Offset 3 beginnt und die Anzahl weiterhin Bytes meint. Der Rich-Text-Leser macht ab Offset 1 im ersten Continue-Segment dasselbe. [MS-XLS] 2.5.293 garantiert bei fHighByte gleich 1, dass der Bruch an einer Double-Byte-Zeichengrenze liegt, daher ist keine Teilzeichenverwaltung nötig. Die Bytearithmetik muss trotzdem stimmen

Was macht GetWideStringWithZero tatsächlich?

TXLSBlob.GetWideStringWithZero ist ein Wide-Character-Leser, der eingebettete NULs behält. Das Suffix WithZero markiert den Erhalt von NULs und nicht die Zeichenbreite: Intern läuft dasselbe SetString über einen PWideChar mit Count div SizeOf(WideChar) wie bei GetWideString, nur ohne Terminatorsuche. Das Einbyte-Gegenstück ist TXLSBlob.GetStringWithZero und liefert einen AnsiString. Im Namen steht nicht, welches welches ist, und diese Mehrdeutigkeit hat im Codebestand echte Fehler verursacht. Die konkrete Fehllektüre lohnt sich, weil sie so plausibel aussieht: GetWideString braucht cch * 2, also muss GetWideStringWithZero derjenige sein, der cch direkt nimmt. Es nimmt tatsächlich cch ohne Beschwerde, liefert einen WideString und der Compiler ist zufrieden. Trotzdem kommen nur halb so viele Zeichen zurück, zusammengesetzt aus den falschen Bytepaaren. Der korrekte 8-Bit-Weg ist TXLSBlob.GetString mit einfachem cch-Bytecount und ein Cast zu WideString bei der Zuweisung. HotXLS 2.376.0 behob genau diesen Missbrauch in zwei Chart-Decodern

SXViewLink und das Längengate je Kodierung

SXViewLink ($0858, [MS-XLS] 2.4.316) ist das sauberste Arbeitsbeispiel, weil es beide Asymmetrien in einem acht Byte langen Header verbindet. Das Layout lautet rt(2), unused(2), reserved(2), cch(1), fHigh(1), gefolgt von einem XLUnicodeStringNoCch-Body: fHigh = 1 bedeutet cch * 2 Bytes UTF-16, fHigh = 0 bedeutet cch Bytes Einbytezeichen, und cch ist auf 255 begrenzt, weil das Längenfeld ein einzelnes Byte ist. HotXLS schreibt den Record in den Chart-Globals vor Units und neben PivotChartBits ($0859, [MS-XLS] 2.4.196), wenn ein Chart-Sheet mit einer PivotTable-Ansicht verknüpft ist; die Recordsicht dieser Mechanik wird beim Schreiben von BIFF8-PivotTable-Records in Delphi behandelt

// SXViewLink ([MS-XLS] 2.4.316): rt(2) unused(2) reserved(2) cch(1)
// danach ein XLUnicodeStringNoCch - fHigh(1) gefolgt von den Zeichen
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;

Die beiden Zweige sind nicht kosmetisch. Eine frühere Version prüfte beide Kodierungen mit dem Wide-Character-Ausdruck 8 + cch * 2, daher scheiterte ein von Excel geschriebener 8-Bit-Ansichtsname am Gate und der Decoder lieferte einen leeren PivotSourceName, während IsPivotChart false blieb. Die Pivot-Verknüpfung verschwand ohne eine einzige Diagnose aus dem Modell. Derselbe Fehler saß im Trendline-Decoder ($2050, [MS-XLS] 2.4.328): Dort folgen auf 28 Bytes numerischen Payloads ein zweibytegroßes cch bei Offset 28, fHigh bei Offset 30 und die Zeichen bei Offset 31. Trendline-Beschriftungen, die Excel mit 8-Bit-Zeichen geschrieben hatte, wurden als leere Strings dekodiert. Beide Fehler wurden in derselben Version behoben. Der 8-Bit-Fall ist außerdem keine historische Kuriosität aus Excel-2.0-bis-4.0-Dateien: Aktuelles Excel schreibt weiterhin 8-Bit-BIFF8-Payloads, sobald jedes Zeichen in ein Byte passt

Wie dekodiert man einen neuen BIFF8-Record sicher in Delphi?

Wenn das Feld tatsächlich ein standardmäßiger XLUnicodeString ist, verwenden Sie TXLSBlob.GetBiffString statt den Zweig selbst zu bauen. Die Methode liest das Längenfeld, das Optionsbyte und wählt den passenden Reader, danach rückt sie den Cursor hinter den Body. Die beiden Boolean-Parameter sind der Teil, den man sorgfältig lesen muss: is8bit beschreibt die Breite des Längenfelds und nicht die Breite der Zeichen, während iswide angibt, ob überhaupt ein fHigh-Optionsbyte folgt. BIFF-Versionen unter $0600 haben keines von beiden

var
  Offset: LongWord;
begin
  Offset := 6;  // im SXViewLink beginnt hier das cch-Byte
  // is8bit = das Längenfeld ist ein Byte breit
  // iswide = hinter dem Längenfeld folgt ein fHigh-Optionsbyte
  Name := Data.GetBiffString(Offset, True, True);
  // Offset zeigt nun auf das erste Byte hinter dem String-Body

Von Hand geschriebene Zweige bleiben sinnvoll, wenn der Handler abgeschnittene oder feindselige Eingaben überstehen muss, denn GetBiffString verlässt sich darauf, dass EnsureReadable auslöst, statt auf eine vom Aufrufer kontrollierte Bounds-Prüfung. Deshalb prüft der HotXLS-Chart-Decoder DataLength und liefert nichts zurück, statt zu werfen: Eine beschädigte Arbeitsmappe eines Drittanbieters soll einen einzelnen Caption-Text kosten und nicht das ganze Dokument. Der Kompromiss ist beabsichtigt. Genau deshalb muss das kodierungsspezifische Gate stimmen, denn es ist das Gate, das aus einem schlechten Lesen Stille macht

Ein letzter Prozesshinweis stammt ebenfalls aus dieser Version und wurde auf die harte Tour gelernt. Prüfen Sie eine gespeicherte und wieder geöffnete Arbeitsmappe und nicht das In-Memory-Modell, das Sie gerade gebaut haben. Der 2.376.0-Batch fand zusätzlich einen SXEx-Emitter ([MS-XLS] 2.4.282), der einen 24-Byte-Body deklarierte, aber nur 22 schrieb, wodurch jeder Record nach der PivotTable-Ansicht fehljustiert wurde, einschließlich Worksheet-EOF und jedem folgenden Chart-Sheet-Substream. Die vorhandenen Pivot-Tests entdeckten das nicht, weil sie alle gegen den Speicher prüften. Für die Stringdekodierung gilt dasselbe: Nur ein Roundtrip durch die Datei testet die Bytezahlen wirklich

Wenn Sie in Delphi oder C++Builder mit klassischen XLS-Interna arbeiten und lieber keinen eigenen BIFF8-Recordreader pflegen möchten, sind die oben beschriebenen Kodierungsregeln bereits in der HotXLS Delphi spreadsheet component implementiert und regressionsgetestet. Sie liest und schreibt XLS und XLSX ohne Excel und ohne OLE-Automatisierung