HotXLS dekóduje XLUnicodeString BIFF8 tak, že nejdříve načte cch a příznak fHigh a potom zvolí reader odpovídající kódování: TXLSBlob.GetWideString s počtem bajtů cch * 2, když je fHigh 1, a TXLSBlob.GetString, když je fHigh 0. Prohoďte tyto dvojice a rekord vrátí prázdný nebo poloviční string, nikdy výjimku
Právě to činí tuto třídu chyb drahou. Graf se otevře, série se vykreslí správně, osy sedí a popisek jedné trendline je prostě prázdný. Nic v logu, nic v exception handleru, žádný dialog o poškozeném souboru. Soubor byl celou dobu v pořádku; reader si vyžádal špatný počet bajtů a dostal přesně to, o co požádal
Proč se řetězec BIFF8 vrátí prázdný
Řetězec BIFF8 se vrátí prázdný, protože length guard odmítl payload ještě před samotným čtením nebo reader skončil u prvního NUL, na který narazil. Obě cesty jsou z konstrukce tiché. V HotXLS je guard obvykle explicitní kontrola DataLength v handleru rekordu a musí se počítat podle kódování: 16bitový payload potřebuje pro tělo SXViewLink 8 + cch * 2 bajtů, zatímco 8bitový payload pouze 8 + cch. Použijte wide-character aritmetiku na 8bitový rekord a každý krátký název selže na gate. Druhou pastí je chování NUL, protože TXLSBlob.GetString i TXLSBlob.GetWideString prohledají dekódovaný výsledek na terminátor a tam ho zkrátí, přičemž když terminátor leží na pozici jedna, vrátí prázdný string. Přečtěte 16bitové tělo s polovičním počtem bajtů a ponecháte prvních cch div 2 znaků; přečtěte 8bitové tělo wide readerem a dvojice bajtů vytvoří libovolné code pointy. Hlasité je pouze příliš dlouhé čtení: TXLSBlob.EnsureReadable vyvolá Blob read exceeds data size, když požadavek překročí velikost blobu. Pro podčtení takový alarm neexistuje
GetWideString počítá bajty, nikoli znaky
TXLSBlob.GetWideString(Index, Count) přijímá Count v bajtech. Uvnitř provede SetString nad PWideChar s Count div SizeOf(WideChar), takže předání počtu znaků string potichu rozpůlí. Layouty rekordů BIFF8 přitom délku řetězce vyjadřují ve znacích. Každé 16bitové call site proto musí převod * 2 nést samo a každé 8bitové ho naopak nesmí nést. Je to stejná hranice kódování, která se objeví při opačném směru, když text zapisujete, a vyplatí se ji číst společně s Unicode-safe exportem spreadsheetů v Delphi, pokud vaše pipeline posílá stringy oběma směry
// 16bitový XLUnicodeStringNoCch: cch znaků, cch * 2 bajtů
Name := Data.GetWideString(Start, cch * 2); // správně
Name := Data.GetWideString(Start, cch); // polovina textu, bez chyby
// 8bitový XLUnicodeStringNoCch: cch znaků, cch bajtů
Name := WideString(Data.GetString(Start, cch)); // správně
Name := Data.GetWideStringWithZero(Start, cch); // stále wide reader
Konvence platí všude, kde se byte stream prochází ručně. Když HotXLS skládá dlouhý String rekord ($0207, [MS-XLS] 2.4.268) znovu z jeho Continue rekordů ($003C), wide větev spočítá segCh z délky segmentu a potom zavolá GetWideString(3, segCh * 2), protože tělo rekordu začíná na offsetu 3 a count je stále v bajtech. Reader rich textu dělá totéž z offsetu 1 na prvním segmentu Continue. [MS-XLS] 2.5.293 zaručuje, že se mez nachází na hranici dvoubajtového znaku, když je fHighByte 1, takže není potřeba evidence částečného znaku, ale bajtovou aritmetiku musíte stále zvládnout správně
Co přesně dělá GetWideStringWithZero
TXLSBlob.GetWideStringWithZero je wide-character reader, který zachovává vložené NUL. Přípona WithZero označuje zachování NUL, nikoli šířku znaku: interně provádí stejné SetString nad PWideChar s Count div SizeOf(WideChar) jako GetWideString, jen bez hledání terminátoru. Jednobajtovým protějškem je TXLSBlob.GetStringWithZero, který vrací AnsiString. V názvu nic neříká, který z nich je který, a tato nejasnost už codebase stála skutečné chyby. Konkrétní záměnu stojí za pojmenování, protože vypadá velmi uvěřitelně: GetWideString potřebuje cch * 2, takže GetWideStringWithZero musí být ten, který přijímá přímo cch. Přijme cch bez stížnosti, vrátí WideString a kompilátor je spokojený. Také vrátí polovinu znaků sestavenou ze špatných dvojic bajtů. Správná 8bitová cesta je TXLSBlob.GetString s prostým počtem cch bajtů, při přiřazení přetypovaná na WideString. HotXLS 2.376.0 opravil právě toto použití ve dvou dekodérech grafů
SXViewLink a length gate podle kódování
SXViewLink ($0858, [MS-XLS] 2.4.316) je nejčistší praktický příklad, protože v jediné osmibajtové hlavičce balí obě asymetrie. Layout je rt(2), unused(2), reserved(2), cch(1), fHigh(1), po kterém následuje tělo XLUnicodeStringNoCch: fHigh = 1 znamená cch * 2 bajtů UTF-16, fHigh = 0 znamená cch jednobajtových znaků a cch je omezeno na 255, protože délkové pole má jediný bajt. HotXLS zapisuje rekord do chart globals před Units, vedle PivotChartBits ($0859, [MS-XLS] 2.4.196), když chart sheet odkazuje na view PivotTable; pohled na úrovni rekordů popisuje zápis rekordů BIFF8 PivotTable v Delphi
// SXViewLink ([MS-XLS] 2.4.316): rt(2) unused(2) reserved(2) cch(1)
// potom XLUnicodeStringNoCch - fHigh(1) následované 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;
Tyto dvě větve nejsou kosmetika. Dřívější verze obě kódování hlídala výrazem 8 + cch * 2 pro wide znaky, takže Excelový 8bitový název view guard neprošel a decoder vrátil prázdný PivotSourceName s IsPivotChart ponechaným false. Pivot link z modelu zmizel bez jediné diagnostiky. Stejná chyba seděla v decoderu Trendline ($2050, [MS-XLS] 2.4.328), kde pole jména následuje po 28 bajtech číselného payloadu, dvoubajtové cch je na offsetu 28, fHigh na offsetu 30 a znaky na offsetu 31; popisky trendline zapsané Excelem 8bitovými znaky se dekódovaly jako prázdné stringy. Obě chyby byly opraveny ve stejném releasu. A 8bitový případ není historická kuriozita omezená na soubory Excelu 2.0 až 4.0: současný Excel stále zapisuje 8bitové payloady BIFF8, kdykoli se každý znak vejde do jednoho bajtu
Jak bezpečně dekódovat nový rekord BIFF8 v Delphi
Tam, kde je pole skutečně standardní XLUnicodeString, použijte TXLSBlob.GetBiffString místo ručního psaní větve. Načte délkové pole, option byte, dispatchne na odpovídající reader a posune kurzor za tělo. Dva Boolean parametry jsou část, kterou je potřeba číst pozorně: is8bit popisuje šířku délkového pole, nikoli šířku znaků, a iswide říká, zda vůbec následuje option byte fHigh. Verze BIFF pod $0600 nemají ani jedno
var
Offset: LongWord;
begin
Offset := 6; // v SXViewLink zde začíná bajt cch
// is8bit = délkové pole má šířku jednoho bajtu
// iswide = za délkovým polem následuje option byte fHigh
Name := Data.GetBiffString(Offset, True, True);
// Offset nyní ukazuje na první bajt za tělem stringu
Ručně psané větve mají stále své místo, když handler musí přežít zkrácený nebo nepřátelský vstup, protože GetBiffString spoléhá na výjimku z EnsureReadable místo na bounds check, který byste řídili. Proto decoder grafu HotXLS hlídá DataLength a vrací nic namísto výjimky: poškozený workbook třetí strany má stát jeden popisek, nikoli celý dokument. Kompromis je záměrný a právě proto musí být guard závislý na kódování správný, protože guard je mechanismus, který špatné čtení mění v ticho
Ještě jeden procesní detail, získaný bolestivě ve stejném releasu. Assertujte nad uloženým a znovu otevřeným workbookem, nikoli nad in-memory modelem, který jste právě vytvořili. Batch 2.376.0 také odhalil emitter SXEx ([MS-XLS] 2.4.282), který deklaroval tělo o 24 bajtech, ale zapsal jen 22, čímž rozhodil každý rekord po view PivotTable včetně worksheet EOF a každého chart sheet substreamu, který následoval. Existující pivot testy to nezachytily, protože všechny assertovaly proti paměti. Dekódování stringů má stejnou vlastnost: jediný test, který skutečně procvičí počty bajtů, je round-trip přes soubor
Pokud v Delphi nebo C++Builderu pracujete s interními strukturami klasického XLS a nechcete udržovat vlastní reader BIFF8 rekordů, výše uvedená pravidla kódování jsou už implementovaná a regresně testovaná v HotXLS Delphi spreadsheet component, který čte a zapisuje XLS a XLSX bez Excelu i bez OLE automation