Teknisk artikkel

BIFF8 XLUnicodeString-dekoding i Delphi: cch og fHigh

HotXLS dekoder en BIFF8 XLUnicodeString ved først å lese cch og fHigh-flagget, og deretter velge leseren som passer kodingen: TXLSBlob.GetWideString med et byteantall på cch * 2 når fHigh er 1, og TXLSBlob.GetString når fHigh er 0. Parer du dem motsatt, gir record-en en tom eller halvlang streng, aldri et unntak

Det er dette som gjør denne typen feil kostbar. Et chart åpnes, seriene tegnes riktig, aksene stemmer og én trendlinje-caption er ganske enkelt blank. Ingenting i loggen, ingenting i exception-handleren, ingen dialog om korrupt fil. Filen var fin hele tiden; leseren ba om feil antall byte og fikk nøyaktig det den ba om

Hvorfor kommer en BIFF8-streng tilbake som tom?

En BIFF8-streng kommer tilbake som tom fordi en lengdevakt avviste payloaden før lesingen i det hele tatt skjedde, eller fordi leseren stoppet ved den første NUL-en den fant. Begge stiene er stille av konstruksjon. I HotXLS er vakten vanligvis en eksplisitt DataLength-kontroll i record-handleren, og den må beregnes per koding: En 16-biters payload trenger 8 + cch * 2 byte for en SXViewLink-kropp, mens en 8-biters payload bare trenger 8 + cch. Bruk den wide-character-aritmetikken på en 8-biters record, så stryker hvert kort navn på porten. NUL-oppførselen er den andre fellen, fordi både TXLSBlob.GetString og TXLSBlob.GetWideString skanner det dekodede resultatet etter en terminator og trunkerer der, og returnerer en tom streng når terminatoren havner på posisjon én. Les en 16-biters kropp med halvparten av byteantallet, og du beholder de første cch div 2-tegnene; les en 8-biters kropp gjennom wide-leseren, og byteparene danner vilkårlige code points. Bare en for lang lesing er høylytt: TXLSBlob.EnsureReadable kaster Blob read exceeds data size når forespørselen går forbi blob-en. Underlesing har ingen slik alarm

GetWideString teller byte, ikke tegn

TXLSBlob.GetWideString(Index, Count) tar Count i byte. Internt kjører den en SetString over en PWideChar med Count div SizeOf(WideChar), så det å sende inn et antall tegn halverer strengen stille. BIFF8-record-layoutene uttrykker samtidig strenglengde i tegn. Hvert 16-biters kallsted må derfor selv bære * 2-konverteringen, og hvert 8-biters kallsted må la være å bære den. Dette er den samme kodingsgrensen som dukker opp når du skriver tekst ut igjen i stedet for å lese den, og det er verdt å lese den sammen med Unicode-sikker regnearkeksport i Delphi hvis pipelinen din flytter strenger i begge retninger

// 16-bit XLUnicodeStringNoCch: cch tegn, cch * 2 byte
Name := Data.GetWideString(Start, cch * 2);        // riktig
Name := Data.GetWideString(Start, cch);            // halvparten av teksten, ingen feil

// 8-bit XLUnicodeStringNoCch: cch tegn, cch byte
Name := WideString(Data.GetString(Start, cch));    // riktig
Name := Data.GetWideStringWithZero(Start, cch);    // fortsatt en wide-leser

Konvensjonen holder overalt der byte-strømmen gås gjennom for hånd. Når HotXLS setter sammen en lang String-record ($0207, [MS-XLS] 2.4.268) fra Continue-recorder ($003C), beregner den wide-grenen segCh fra segmentlengden og kaller deretter GetWideString(3, segCh * 2), fordi record-kroppen starter på offset 3 og antallet fortsatt er byte. Rich-text-leseren gjør det samme fra offset 1 på det første Continue-segmentet. [MS-XLS] 2.5.293 garanterer at bruddet ligger på en dobbeltbyte-tegngrense når fHighByte er 1, så det trengs ingen bokføring av delte tegn, men byte-aritmetikken er fortsatt din å få riktig

Hva gjør GetWideStringWithZero egentlig?

TXLSBlob.GetWideStringWithZero er en wide-character-leser som beholder innebygde NUL-er. Suffikset WithZero markerer at NUL-er beholdes, ikke tegnbredden: Internt kjører den den samme SetString-operasjonen over en PWideChar med Count div SizeOf(WideChar) som GetWideString, minus terminatorskanningen. Motstykket på én byte er TXLSBlob.GetStringWithZero, som returnerer en AnsiString. Ingenting i navnet sier hvilken som er hvilken, og denne tvetydigheten har kostet kodebasen reelle feil. Den konkrete feillesingen er verdt å navngi, fordi den ser så plausibel ut: GetWideString trenger cch * 2, så GetWideStringWithZero må være den som tar cch direkte. Den tar imot cch uten klage, returnerer en WideString og gjør kompilatoren fornøyd. Den returnerer også halvparten av tegnene, satt sammen fra feil bytepar. Den korrekte 8-biters stien er TXLSBlob.GetString med et vanlig cch-byteantall, castet til WideString ved tildelingen. HotXLS 2.376.0 rettet nøyaktig dette misbruket i to chart-dekodere

SXViewLink og lengdevakten per koding

SXViewLink ($0858, [MS-XLS] 2.4.316) er det reneste gjennomarbeidede eksempelet, fordi den pakker begge asymmetriene inn i én header på åtte byte. Layouten er rt(2), unused(2), reserved(2), cch(1), fHigh(1), fulgt av en XLUnicodeStringNoCch-kropp: fHigh = 1 betyr cch * 2 byte UTF-16, fHigh = 0 betyr cch byte med ett-byte-tegn, og cch er begrenset til 255 fordi lengdefeltet er én byte. HotXLS skriver record-en inn i chart-globals før Units, ved siden av PivotChartBits ($0859, [MS-XLS] 2.4.196), når et chart sheet lenker til en PivotTable-visning; record-perspektivet på denne maskineriet dekkes i skriving av BIFF8 PivotTable-recorder i Delphi

// SXViewLink ([MS-XLS] 2.4.316): rt(2) unused(2) reserved(2) cch(1)
// deretter en XLUnicodeStringNoCch - fHigh(1) fulgt av 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 grenene er ikke kosmetiske. En tidligere versjon vaktsjekket begge kodingene med wide-character-uttrykket 8 + cch * 2, så et 8-biters view-navn skrevet av Excel strøk på vakten, og dekoderen returnerte en tom PivotSourceName med IsPivotChart stående false. Pivot-lenken forsvant fra modellen uten én eneste diagnose. Den identiske feilen lå i Trendline-dekoderen ($2050, [MS-XLS] 2.4.328), der navnefeltet følger 28 byte med numerisk payload, med en 2-byte cch ved offset 28, fHigh ved offset 30 og tegnene ved offset 31; trendline-captioner som Excel hadde skrevet med 8-biters tegn, ble dekodet som tomme strenger. Begge ble rettet i samme release. Og 8-biters tilfellet er ikke en gammel kuriositet begrenset til Excel 2.0 til 4.0-filer: Dagens Excel skriver fortsatt 8-biters BIFF8-payload-er når hvert tegn passer i én byte

Hvordan dekoder du en ny BIFF8-record trygt i Delphi?

Der feltet virkelig er en standard XLUnicodeString, bør du bruke TXLSBlob.GetBiffString i stedet for å håndskrive grenen. Den leser lengdefeltet, leser option-byte-en, sender videre til den passende leseren og flytter markøren forbi kroppen. De to boolske parameterne er delen du må lese nøye: is8bit beskriver bredden på lengdefeltet, ikke bredden på tegnene, og iswide sier om en fHigh-option-byte i det hele tatt er til stede. BIFF-versjoner under $0600 har ingen av delene

var
  Offset: LongWord;
begin
  Offset := 6;  // i SXViewLink starter cch-byte-en her
  // is8bit = lengdefeltet er én byte bredt
  // iswide = en fHigh-option-byte følger etter lengdefeltet
  Name := Data.GetBiffString(Offset, True, True);
  // Offset peker nå på den første byte-en etter strengkroppen

Håndskrevne grener har fortsatt sin plass når handleren må tåle avkortet eller fiendtlig input, fordi GetBiffString baserer seg på at EnsureReadable kaster i stedet for på en grensekontroll du styrer. Det er derfor HotXLS-chart-dekoderen vaktsjekker DataLength og returnerer ingenting i stedet for å kaste: En feilformet arbeidsbok fra en tredjepart bør koste deg én caption, ikke hele dokumentet. Avveiingen er bevisst, og det er nettopp derfor den kodingsspesifikke vakten må være riktig, siden vakten er det som gjør en dårlig lesing om til stillhet

Én siste prosessdetalj, lært på den harde måten i samme release. Assert på en lagret og gjenåpnet arbeidsbok, ikke på modellen i minnet du nettopp bygget. Batch-en i 2.376.0 avdekket også en SXEx-emitter ([MS-XLS] 2.4.282) som deklarerte en kropp på 24 byte, men bare skrev 22, og feiljusterte hver record etter PivotTable-visningen, inkludert worksheet EOF og enhver chart sheet-understrøm som fulgte. De eksisterende pivottestene fanget det aldri fordi de alle hevdet mot minne. Strengdekoding har samme egenskap: En tur-retur gjennom filen er den eneste testen som faktisk utøver byteantallet

Hvis du arbeider med klassiske XLS-internals i Delphi eller C++Builder og helst ikke vil vedlikeholde din egen BIFF8-record-leser, er kodingsreglene over allerede implementert og regresjonstestet i HotXLS Delphi spreadsheet-komponenten, som leser og skriver XLS og XLSX uten Excel eller OLE-automatisering