A HotXLS egy BIFF8 XLUnicodeString dekódolásakor előbb a cch értéket és az fHigh flaget olvassa, majd a kódoláshoz illő olvasót választja: fHigh = 1 esetén a TXLSBlob.GetWideString-et cch * 2 bájtos számlálóval, fHigh = 0 esetén a TXLSBlob.GetString-et. Ha fordítva párosítod őket, a rekord üres vagy fele olyan hosszú stringet ad, kivétel nélkül
Ez teszi drágává ezt a hibatípust. Egy chart megnyílik, a sorozatok rendben kirajzolódnak, a tengelyek jók, egy trendline-felirat pedig egyszerűen üres. Nincs log, nincs exception handler, nincs corrupt-file dialógus. A fájl végig rendben volt; az olvasó rossz bájtszámot kért, és pontosan azt kapta, amit kért
Miért jön vissza üresen egy BIFF8-string?
Egy BIFF8-string azért jön vissza üresen, mert a length guard elutasította a payloadot, mielőtt az olvasás megtörtént, vagy mert az olvasó az első NUL-nál leállt. Mindkét út természeténél fogva csendes. A HotXLS-ben a guard rendszerint explicit DataLength-ellenőrzés a rekordkezelőben, és kódolásonként kell kiszámítani: egy 16 bites payloadnak egy SXViewLink törzsénél 8 + cch * 2 bájtra van szüksége, egy 8 bitesnek viszont csak 8 + cch-re. Alkalmazd a wide-character számtant egy 8 bites rekordra, és minden rövid név elbukik a kapun. A második csapda a NUL-viselkedés, mert a TXLSBlob.GetString és a TXLSBlob.GetWideString egyaránt terminátort keres a dekódolt eredményben, és ott levágja, üres stringet adva, ha a terminátor az első pozícióra kerül. Ha egy 16 bites törzset fél bájtszámmal olvasol, az első cch div 2 karakter marad meg; ha egy 8 bites törzset a wide readeren keresztül olvasol, a bájtpárok tetszőleges code pointokat alkotnak. Csak a túl hosszú olvasás hangos: a TXLSBlob.EnsureReadable Blob read exceeds data size hibát emel, ha a kérés túlfut a blobon. Az alulolvasásnak nincs ilyen riasztása
A GetWideString byte-okat számol, nem karaktereket
A TXLSBlob.GetWideString(Index, Count) a Count értékét byte-ban kapja. Belsőleg SetString-et futtat egy PWideChar-on, Count div SizeOf(WideChar) mennyiséggel, ezért ha karakterhosszt adsz át, csendben megfelezed a stringet. A BIFF8 rekordlayoutok eközben karakterekben fejezik ki a string hosszát. Minden 16 bites call site-nak tehát magának kell hordoznia a * 2 konverziót, minden 8 bitesnek pedig nem szabad hordoznia. Ez ugyanaz a kódolási határ, amely akkor is előjön, amikor nem olvasod, hanem kiírod a szöveget, ezért érdemes együtt olvasni az Unicode-biztonságos spreadsheet-exportról szóló Delphi-cikket, ha a pipeline mindkét irányban mozgat stringeket
// 16 bites XLUnicodeStringNoCch: cch karakter, cch * 2 byte
Name := Data.GetWideString(Start, cch * 2); // helyes
Name := Data.GetWideString(Start, cch); // a szöveg fele, hiba nélkül
// 8 bites XLUnicodeStringNoCch: cch karakter, cch byte
Name := WideString(Data.GetString(Start, cch)); // helyes
Name := Data.GetWideStringWithZero(Start, cch); // még mindig wide reader
A konvenció mindenhol érvényes, ahol kézzel járjuk be a byte-streamet. Amikor a HotXLS egy hosszú String rekordot ($0207, [MS-XLS] 2.4.268) a Continue rekordjaiból ($003C) rak össze, a wide ág a szegmenshosszból számítja a segCh-t, majd GetWideString(3, segCh * 2)-t hív, mert a rekordtörzs a 3. offseten indul, a számláló pedig továbbra is byte. A rich-text reader ugyanezt teszi az első Continue szegmens 1. offsetjéről. Az [MS-XLS] 2.5.293 garantálja, hogy a törés kétbájtos karakterhatárra esik, amikor fHighByte = 1, ezért nincs szükség részkarakter-kezelésre, a byte-számtan helyessége viszont továbbra is a te feladatod
Mit csinál valójában a GetWideStringWithZero?
A TXLSBlob.GetWideStringWithZero olyan wide-character olvasó, amely megtartja a beágyazott NUL-okat. A WithZero utótag a NUL megőrzését jelöli, nem a karakter szélességét: belsőleg ugyanazt a SetString-et futtatja PWideChar-on, Count div SizeOf(WideChar) mennyiséggel, mint a GetWideString, csak a terminátorkeresés marad el. Az egybájtos párja a TXLSBlob.GetStringWithZero, amely AnsiString-et ad vissza. A névből semmi nem mondja meg, melyik melyik, ez pedig valós hibákba került már ebben a kódbázisban. A konkrét félreolvasást érdemes megnevezni, mert nagyon hihető: a GetWideString cch * 2-t igényel, ezért a GetWideStringWithZero biztosan az, amelyik közvetlenül cch-t kap. Panasz nélkül elfogadja a cch-t, WideString-et ad vissza, a compiler pedig elégedett. A karakterek felét kapod vissza, rossz bájtpárokból összerakva. A helyes 8 bites útvonal a TXLSBlob.GetString sima cch byte-számlálóval, majd az eredmény hozzárendeléskor WideString-gé castolva. A HotXLS 2.376.0 pontosan ezt a hibás használatot javította két chart-dekóderben
SXViewLink és a kódolásonkénti hosszkapu
Az SXViewLink ($0858, [MS-XLS] 2.4.316) a legtisztább működő példa, mert egy nyolcbájtos fejlécbe mindkét aszimmetriát beleteszi. Az elrendezés rt(2), unused(2), reserved(2), cch(1), fHigh(1), majd egy XLUnicodeStringNoCch törzs: fHigh = 1 UTF-16-ból cch * 2 byte-ot jelent, fHigh = 0 egybájtos karakterekből cch byte-ot, a cch pedig 255-re korlátozott, mert a hosszmező egyetlen bájt. A HotXLS a rekordot a chart globalokba írja a Units elé, a PivotChartBits ($0859, [MS-XLS] 2.4.196) mellé, amikor egy chartsheet PivotTable-view-ra linkel; ennek rekordszintű gépezetét a BIFF8 PivotTable-rekordok Delphiből írásáról szóló cikk fedi le
// SXViewLink ([MS-XLS] 2.4.316): rt(2) unused(2) reserved(2) cch(1)
// ezután egy XLUnicodeStringNoCch - fHigh(1), majd a karakterek
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;
A két ág nem kozmetika. Egy korábbi változat mindkét kódolást a wide-character 8 + cch * 2 kifejezés mögé zárta, így egy Excel által írt, 8 bites view-név elbukott a guardon, a dekóder üres PivotSourceName-et adott vissza, az IsPivotChart pedig false maradt. A pivot link diagnosztika nélkül tűnt el a modellből. Ugyanez a hiba ült a Trendline dekóderben ($2050, [MS-XLS] 2.4.328), ahol a névmezőt 28 bájt numerikus payload előzi meg, a kétbájtos cch a 28. offseten, az fHigh a 30.-on, a karakterek pedig a 31.-en vannak; az Excel által 8 bites karakterekkel írt trendline-feliratok üres stringként dekódolódtak. Mindkettő ugyanabban a kiadásban javult. És a 8 bites eset nem régi különlegesség, amely az Excel 2.0–4.0 fájlokra korlátozódik: a jelenlegi Excel ma is 8 bites BIFF8 payloadokat ír, amikor minden karakter elfér egy byte-on
Hogyan dekódolj biztonságosan új BIFF8-rekordot Delphiben?
Ha a mező valóban szabványos XLUnicodeString, használd a TXLSBlob.GetBiffString-et a kézzel írt ág helyett. Beolvassa a hosszmezőt, az opcióbyte-ot, a megfelelő olvasóra dispatchol, és a kurzort a törzs utáni bájtra lépteti. A két Boolean paramétert kell a legfigyelmesebben olvasni: az is8bit a hosszmező szélességét írja le, nem a karakterekét, az iswide pedig azt mondja meg, hogy egyáltalán jelen van-e fHigh opcióbyte. A $0600 alatti BIFF-verziókban egyik sincs
var
Offset: LongWord;
begin
Offset := 6; // SXViewLink esetén itt kezdődik a cch byte
// is8bit = a hosszmező egy bájt széles
// iswide = fHigh opcióbyte követi a hosszmezőt
Name := Data.GetBiffString(Offset, True, True);
// Offset most a string body utáni első byte-ra mutat
A kézzel írt ágak akkor is indokoltak, ha a handlernek csonkolt vagy ellenséges bemenetet kell túlélnie, mert a GetBiffString az általad vezérelt bounds-check helyett az EnsureReadable kivételére támaszkodik. Ezért kapuzik a HotXLS chart-dekódere DataLength-del, és inkább semmit ad vissza, mint hogy kivételt dobjon: egy hibás külső workbook kerüljön egyetlen feliratba, ne az egész dokumentumba. A kompromisszum szándékos, és pontosan ezért kell helyesnek lennie a kódolásspecifikus guardnak, mert a guard alakítja a rossz olvasást csenddé
Van még egy folyamatbeli részlet, amelyet ugyanebben a kiadásban tanultunk meg nehezen. Elmentett és újranyitott munkafüzeten assertionelj, ne azon az in-memory modellen, amelyet éppen felépítettél. A 2.376.0 batch egy olyan SXEx emittert ([MS-XLS] 2.4.282) is felfedett, amely 24 bájtos törzset deklarált, de csak 22-t írt, és eltolta a PivotTable-view utáni összes rekordot, a worksheet EOF-et és minden következő chart-sheet alstreamet is. A meglévő pivotteszteket ez nem kapta el, mert mind memórián ellenőrzött. A stringdekódolás ugyanezt tudja: a fájlon átmenő round-trip az egyetlen teszt, amely ténylegesen gyakorolja a bájtszámokat
Ha klasszikus XLS-belsőkkel dolgozol Delphiben vagy C++Builderben, és nem szeretnél saját BIFF8-rekordolvasót karbantartani, a fenti kódolási szabályokat a HotXLS Delphi spreadsheet component már megvalósítja és regresszióteszteli, XLS és XLSX olvasásával és írásával Excel vagy OLE automation nélkül