Odborný článok

Dekódovanie XLUnicodeString v BIFF8 v Delphi: cch a fHigh

HotXLS dekóduje BIFF8 XLUnicodeString tak, že najprv prečíta cch a flag fHigh a potom vyberie reader zodpovedajúci kódovaniu: TXLSBlob.GetWideString s počtom bajtov cch * 2, keď je fHigh 1, a TXLSBlob.GetString, keď je fHigh 0. Spárujte ich opačne a record vráti prázdny alebo polovične dlhý string, nikdy nie výnimku

Práve preto je táto trieda chyby drahá. Graf sa otvorí, série sa vykreslia správne, osi sú správne a jeden popis trendline je jednoducho prázdny. V logu nič, v exception handleri nič, žiadny dialóg o poškodenom súbore. Súbor bol celý čas v poriadku; reader si vypýtal nesprávny počet bajtov a dostal presne to, o čo požiadal

Prečo sa string BIFF8 vráti prázdny?

String BIFF8 sa vráti prázdny preto, že length guard odmietol payload ešte pred samotným čítaním alebo preto, že reader sa zastavil na prvom NUL, ktorý našiel. Obe cesty sú zo svojej konštrukcie tiché. V HotXLS je guard zvyčajne explicitná kontrola DataLength v handleri recordu a musí sa počítať podľa kódovania: 16-bitový payload potrebuje pre telo SXViewLink 8 + cch * 2 bajtov, zatiaľ čo 8-bitový payload iba 8 + cch. Použite široko-znakovú aritmetiku na 8-bitový record a každý krátky názov zlyhá na bráne. Správanie NUL je druhá pasca, pretože TXLSBlob.GetString aj TXLSBlob.GetWideString hľadajú v dekódovanom výsledku terminátor a tam string skrátia; keď terminátor dopadne na pozíciu jedna, vrátia prázdny string. Prečítajte 16-bitové telo s polovičným počtom bajtov a ponecháte prvých cch div 2 znakov; prečítajte 8-bitové telo cez wide reader a dvojice bajtov vytvoria ľubovoľné code points. Hlasné je iba príliš dlhé čítanie: TXLSBlob.EnsureReadable vyvolá Blob read exceeds data size, keď požiadavka prejde za blob. Pri príliš krátkom čítaní taký alarm nie je

GetWideString počíta bajty, nie znaky

TXLSBlob.GetWideString(Index, Count) prijíma Count v bajtoch. Vnútri vykoná SetString nad PWideChar s Count div SizeOf(WideChar), takže odovzdanie počtu znakov string potichu skráti na polovicu. Layouty recordov BIFF8 pritom vyjadrujú dĺžku stringu v znakoch. Každé 16-bitové call site preto musí niesť konverziu * 2 samo a každé 8-bitové ju niesť nesmie. Je to rovnaká hranica kódovania, na ktorú narazíte, keď text znova zapisujete namiesto čítania, takže ak váš pipeline presúva stringy oboma smermi, oplatí sa to čítať spolu s článkom o Unicode-safe exporte spreadsheetov v Delphi

// 16-bitový XLUnicodeStringNoCch: cch znakov, cch * 2 bajtov
Name := Data.GetWideString(Start, cch * 2);        // správne
Name := Data.GetWideString(Start, cch);            // polovica textu, bez chyby

// 8-bitový XLUnicodeStringNoCch: cch znakov, cch bajtov
Name := WideString(Data.GetString(Start, cch));    // správne
Name := Data.GetWideStringWithZero(Start, cch);    // stále wide reader

Konvencia platí všade, kde sa byte stream prechádza ručne. Keď HotXLS zliepa dlhý String record ($0207, [MS-XLS] 2.4.268) späť z jeho Continue recordov ($003C), wide vetva vypočíta segCh z dĺžky segmentu a potom zavolá GetWideString(3, segCh * 2), pretože telo recordu začína na offsete 3 a Count je stále v bajtoch. Rich-text reader robí to isté z offsetu 1 na prvom Continue segmente. [MS-XLS] 2.5.293 garantuje, že hranica leží pri fHighByte 1 na hranici double-byte znaku, takže nie je potrebné účtovanie čiastočných znakov, ale bajtovú aritmetiku stále musíte zvládnuť správne

Čo vlastne robí GetWideStringWithZero?

TXLSBlob.GetWideStringWithZero je wide-character reader, ktorý zachováva vložené NUL. Suffix WithZero označuje zachovanie NUL, nie šírku znaku: vnútri spúšťa rovnaký SetString nad PWideChar s Count div SizeOf(WideChar) ako GetWideString, iba bez skenovania terminátora. Jednobajtový náprotivok je TXLSBlob.GetStringWithZero, ktorý vracia AnsiString. Názov nehovorí, ktorý je ktorý, a táto nejednoznačnosť už kódovú základňu stála skutočné bugy. Konkrétne nesprávne čítanie sa oplatí pomenovať, pretože vyzerá úplne vierohodne: GetWideString potrebuje cch * 2, takže GetWideStringWithZero musí byť ten, ktorý prijíma cch priamo. Naozaj bez sťažnosti prijme cch, naozaj vráti WideString a compiler je spokojný. Zároveň vráti polovicu znakov zostavenú z nesprávnych dvojíc bajtov. Správna 8-bitová cesta je TXLSBlob.GetString s obyčajným byte count cch, pri priradení pretypovaným na WideString. HotXLS 2.376.0 opravil presne toto zneužitie v dvoch chart dekóderoch

SXViewLink a length gate podľa kódovania

SXViewLink ($0858, [MS-XLS] 2.4.316) je najčistejší pracovný príklad, pretože balí obe asymetrie do jednej osem-bajtovej hlavičky. Layout je rt(2), unused(2), reserved(2), cch(1), fHigh(1), po ktorom nasleduje telo XLUnicodeStringNoCch: fHigh = 1 znamená cch * 2 bajtov UTF-16, fHigh = 0 znamená cch jednobajtových znakov a cch je obmedzené na 255, pretože length field je jeden bajt. HotXLS zapisuje record do chart globals pred Units, vedľa PivotChartBits ($0859, [MS-XLS] 2.4.196), keď chart sheet odkazuje na view PivotTable; record-level pohľad na tento mechanizmus pokrýva článok o zápise BIFF8 PivotTable recordov v Delphi

// SXViewLink ([MS-XLS] 2.4.316): rt(2) unused(2) reserved(2) cch(1)
// potom XLUnicodeStringNoCch - fHigh(1), za ktorým nasledujú znaky
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;

Obe vetvy nie sú kozmetické. Skoršia verzia chránila obe kódovania výrazom 8 + cch * 2 pre wide znaky, takže Excelom zapísaný 8-bitový názov view neprešiel guardom a dekóder vrátil prázdny PivotSourceName s IsPivotChart ponechaným na false. Pivot link zmizol z modelu bez jediného diagnostického signálu. Rovnaká chyba sedela v Trendline dekóderi ($2050, [MS-XLS] 2.4.328), kde name field nasleduje po 28 bajtoch numerického payloadu s dvojbajtovým cch na offsete 28, fHigh na offsete 30 a znakmi na offsete 31; popisy trendline, ktoré Excel zapísal s 8-bitovými znakmi, sa dekódovali ako prázdne stringy. Obe chyby sa opravili v tom istom release. A 8-bitový prípad nie je legacy zvedavosť obmedzená na súbory Excel 2.0 až 4.0: aktuálny Excel stále zapisuje 8-bitové payloady BIFF8 vždy, keď sa každý znak zmestí do jedného bajtu

Ako v Delphi bezpečne dekódovať nový record BIFF8?

Tam, kde je pole skutočne štandardný XLUnicodeString, použite TXLSBlob.GetBiffString namiesto ručného vetvenia. Prečíta length field, prečíta option byte, dispatchne na zodpovedajúci reader a posunie kurzor za telo. Dva Boolean parametre sú časť, ktorú treba čítať pozorne: is8bit opisuje šírku length fieldu, nie šírku znakov, a iswide hovorí, či vôbec existuje option byte fHigh za length fieldom. BIFF verzie pod $0600 nemajú ani jedno

var
  Offset: LongWord;
begin
  Offset := 6;  // v SXViewLink sa tu začína byte cch
  // is8bit = length field je široký jeden bajt
  // iswide = za length fieldom nasleduje option byte fHigh
  Name := Data.GetBiffString(Offset, True, True);
  // Offset teraz ukazuje na prvý bajt za telom stringu

Ručne písané vetvy majú stále zmysel, keď handler musí prežiť skrátený alebo nepriateľský input, pretože GetBiffString sa spolieha na to, že EnsureReadable vyvolá chybu, nie na bounds check, ktorý riadite vy. Preto chart dekóder HotXLS stráži DataLength a namiesto exception vráti nič: chybný workbook tretej strany vás má stáť jeden popis, nie celý dokument. Trade-off je zámerný a presne preto musí byť guard špecifický pre kódovanie správny, pretože guard je to, čo zmení zlý read na ticho

Posledná procesná vec, ktorú priniesol rovnaký release ťažko naučená. Assertujte nad uloženým a znovu otvoreným workbookom, nie nad in-memory modelom, ktorý ste práve vytvorili. Batch 2.376.0 zároveň odhalil emitter SXEx ([MS-XLS] 2.4.282), ktorý deklaroval 24-bajtové telo a zapísal iba 22, čím zle zarovnal každý record po pohľade PivotTable vrátane worksheet EOF a každého nasledujúceho chart sheet substreamu. Existujúce pivot testy ho nikdy nezachytili, pretože všetky assertovali proti pamäti. Dekódovanie stringov má rovnakú vlastnosť: round trip cez súbor je jediný test, ktorý skutočne precvičí počty bajtov

Ak v Delphi alebo C++Builderi pracujete s internými štruktúrami klasického XLS a nechcete udržiavať vlastný reader recordov BIFF8, pravidlá kódovania vyššie sú už implementované a regresne testované v HotXLS Delphi spreadsheet component, ktorý číta a zapisuje XLS aj XLSX bez Excelu a bez akejkoľvek OLE automation