HotXLS dekodira BIFF8 XLUnicodeString tako što prvo čita cch i zastavicu fHigh, a zatim bira čitač koji odgovara kodiranju: TXLSBlob.GetWideString sa brojem bajtova cch * 2 kada je fHigh 1 i TXLSBlob.GetString kada je fHigh 0. Uparite ih obrnutim redom i zapis vraća prazan string ili string polovične dužine, nikada izuzetak
Upravo to čini ovu klasu greške skupom. Grafikon se otvori, serije se iscrtaju kako treba, ose su ispravne, a jedan caption trendline-a jednostavno je prazan. Ništa u logu, ništa u exception handler-u, nikakav dijalog o korumpiranom fajlu. Fajl je sve vreme bio ispravan; reader je zatražio pogrešan broj bajtova i dobio tačno ono što je tražio
Zašto se BIFF8 string vraća prazan?
BIFF8 string vraća se prazan zato što je length guard odbio payload pre nego što je čitanje uopšte počelo ili zato što se reader zaustavio na prvom NUL-u koji je pronašao. Obe putanje su po konstrukciji tihe. U HotXLS-u guard je obično eksplicitna provera DataLength u handler-u zapisa i mora da se računa po kodiranju: 16-bitni payload za SXViewLink telo zahteva 8 + cch * 2 bajtova, dok 8-bitni payload traži samo 8 + cch. Primeni wide aritmetiku na 8-bitni zapis i svako kratko ime pada na proveri. NUL ponašanje je druga zamka, jer i TXLSBlob.GetString i TXLSBlob.GetWideString skeniraju dekodirani rezultat za terminator i skraćuju ga tamo, vraćajući prazan string kada terminator padne na prvu poziciju. Pročitajte 16-bitno telo sa polovinom broja bajtova i zadržaćete prvih cch div 2 znakova; pročitajte 8-bitno telo kroz wide reader i parovi bajtova će formirati proizvoljne code point-e. Samo predugačko čitanje je glasno: TXLSBlob.EnsureReadable podiže Blob read exceeds data size kada zahtev izađe iza bloba. Premalo čitanje nema takav alarm
GetWideString broji bajtove, a ne znakove
TXLSBlob.GetWideString(Index, Count) prima Count u bajtovima. Interno radi SetString nad PWideChar sa Count div SizeOf(WideChar), pa prosleđivanje broja znakova nečujno prepolovi string. BIFF8 layout-i zapisa, u međuvremenu, izražavaju dužinu stringa u znakovima. Svako 16-bitno call site zato mora sam da nosi konverziju * 2, a svako 8-bitno mesto ne sme da je nosi. To je ista granica kodiranja koja se pojavljuje kada tekst ponovo upisujete, a ne čitate, pa je vredi pročitati uz Unicode-bezbedan izvoz spreadsheet-a u Delphi-ju ako vaš pipeline pomera stringove u oba smera
// 16-bitni XLUnicodeStringNoCch: cch znakova, cch * 2 bajtova
Name := Data.GetWideString(Start, cch * 2); // ispravno
Name := Data.GetWideString(Start, cch); // polovina teksta, bez greške
// 8-bitni XLUnicodeStringNoCch: cch znakova, cch bajtova
Name := WideString(Data.GetString(Start, cch)); // ispravno
Name := Data.GetWideStringWithZero(Start, cch); // i dalje wide reader
Konvencija važi svuda gde se byte stream ručno obilazi. Kada HotXLS ponovo sklapa dugačak String zapis ($0207, [MS-XLS] 2.4.268) iz njegovih Continue zapisa ($003C), wide grana računa segCh iz dužine segmenta i zatim poziva GetWideString(3, segCh * 2), jer telo zapisa počinje na offsetu 3, a count je i dalje u bajtovima. Reader rich-text-a radi isto sa offseta 1 na prvom Continue segmentu. [MS-XLS] 2.5.293 garantuje da se prelom nalazi na granici dvobajtnog znaka kada je fHighByte 1, pa nije potrebno knjigovodstvo delimičnog znaka, ali bajtovnu aritmetiku i dalje morate pogoditi
Šta zapravo radi GetWideStringWithZero?
TXLSBlob.GetWideStringWithZero jeste wide-character reader koji čuva ugrađene NUL-ove. Sufiks WithZero označava zadržavanje NUL-a, a ne širinu znaka: interno radi isti SetString nad PWideChar sa Count div SizeOf(WideChar) kao i GetWideString, samo bez skeniranja terminatora. Jednobajtni parnjak je TXLSBlob.GetStringWithZero, koji vraća AnsiString. Ime ne kaže koji je koji, a ta dvosmislenost je ovom codebase-u donela stvarne bagove. Vredi imenovati konkretno pogrešno čitanje, jer izgleda sasvim uverljivo: GetWideString traži cch * 2, pa GetWideStringWithZero sigurno prima cch direktno. Zaista prihvata cch bez prigovora, zaista vraća WideString i kompajler je zadovoljan. Takođe vraća polovinu znakova, sastavljenih iz pogrešnih parova bajtova. Ispravna 8-bitna putanja jeste TXLSBlob.GetString sa običnim cch brojem bajtova, uz cast u WideString pri dodeli. HotXLS 2.376.0 popravio je upravo tu zloupotrebu u dva chart decoder-a
SXViewLink i length gate po kodiranju
SXViewLink ($0858, [MS-XLS] 2.4.316) jeste najčistiji radni primer, jer obe asimetrije pakuje u jedno zaglavlje od osam bajtova. Layout je rt(2), unused(2), reserved(2), cch(1), fHigh(1), za kojim sledi telo XLUnicodeStringNoCch: fHigh = 1 znači cch * 2 bajtova UTF-16, fHigh = 0 znači cch bajtova jednobajtnih znakova, a cch je ograničen na 255 jer je polje dužine jedan bajt. HotXLS upisuje zapis u chart globals pre Units, pored PivotChartBits ($0859, [MS-XLS] 2.4.196), kada chart sheet povezuje PivotTable view; pogled na nivo zapisa te mehanike obrađen je u tekstu o upisu BIFF8 PivotTable zapisa u Delphi-ju
// SXViewLink ([MS-XLS] 2.4.316): rt(2) unused(2) reserved(2) cch(1)
// zatim XLUnicodeStringNoCch - fHigh(1), pa znakovi
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;
Dve grane nisu kozmetika. Ranija verzija je obe kodne širine štitila izrazom 8 + cch * 2, pa je Excel-om upisano 8-bitno view ime padalo na proveri, a decoder je vraćao prazan PivotSourceName sa IsPivotChart ostavljenim na false. Pivot link je nestajao iz modela bez ijedne dijagnostike. Ista greška sedela je u Trendline decoder-u ($2050, [MS-XLS] 2.4.328), gde polje imena sledi za 28 bajtova numeričkog payload-a, sa dvobajtnim cch na offsetu 28, fHigh na offsetu 30 i znakovima na offsetu 31; trendline caption-i koje je Excel upisao 8-bitnim znacima dekodirali su se kao prazni stringovi. Obe greške popravljene su u istom izdanju. A 8-bitni slučaj nije istorijska zanimljivost ograničena na Excel fajlove od 2.0 do 4.0: aktuelni Excel i dalje upisuje 8-bitne BIFF8 payload-e kad god svaki znak staje u jedan bajt
Kako bezbedno dekodirati novi BIFF8 zapis u Delphi-ju?
Tamo gde je polje zaista standardni XLUnicodeString koristite TXLSBlob.GetBiffString umesto ručnog grananja. On čita length polje, option bajt, šalje obradu odgovarajućem reader-u i pomera kursor iza tela. Dva Boolean parametra su deo koji treba pažljivo pročitati: is8bit opisuje širinu length polja, a ne širinu znakova, dok iswide kaže da li option bajt fHigh uopšte postoji. BIFF verzije ispod $0600 nemaju ni jedno ni drugo
var
Offset: LongWord;
begin
Offset := 6; // u SXViewLink-u cch bajt počinje ovde
// is8bit = length polje široko je jedan bajt
// iswide = posle length polja sledi fHigh option bajt
Name := Data.GetBiffString(Offset, True, True);
// Offset sada pokazuje na prvi bajt iza tela stringa
Ručne grane i dalje imaju smisla kada handler mora da preživi skraćen ili zlonameran ulaz, jer se GetBiffString oslanja na to da EnsureReadable podigne grešku, a ne na bounds check pod vašom kontrolom. Zato HotXLS chart decoder proverava DataLength i vraća ništa umesto da podigne grešku: neispravna radna sveska treće strane treba da vas košta jednog caption-a, a ne celog dokumenta. Kompromis je nameran i upravo zato encoding-specific guard mora biti tačan, jer je guard ono što loše čitanje pretvara u tišinu
Još jedan procesni detalj, naučen na teži način u istom izdanju. Assertujte sačuvanu i ponovo otvorenu radnu svesku, a ne in-memory model koji ste upravo izgradili. Batch 2.376.0 otkrio je i SXEx emitter ([MS-XLS] 2.4.282) koji je deklarisao telo od 24 bajta, a upisivao samo 22, pogrešno poravnavajući svaki zapis posle PivotTable view-a, uključujući worksheet EOF i svaki chart sheet podstream koji je sledio. Postojeći pivot testovi to nikada nisu uhvatili jer su svi proveravali memoriju. Dekodiranje stringa ima istu osobinu: round trip kroz fajl jedini je test koji zaista vežba brojanje bajtova
Ako radite sa klasičnim XLS internals u Delphi-ju ili C++Builder-u i ne želite da održavate sopstveni BIFF8 record reader, pravila kodiranja iznad već su implementirana i pokrivena regresijama u HotXLS Delphi spreadsheet komponenti, koja čita i upisuje XLS i XLSX bez Excel-a i bilo kakve OLE automatizacije