Tehnički članak

Dekodiranje BIFF8 XLUnicodeString u Delphiju: cch i fHigh

HotXLS dekodira BIFF8 XLUnicodeString tako da najprije pročita cch i zastavicu fHigh, a zatim odabere čitač koji odgovara kodiranju: TXLSBlob.GetWideString s brojem bajtova cch * 2 kada je fHigh 1, a TXLSBlob.GetString kada je fHigh 0. Uparite ih obrnuto i zapis vrati prazan ili napola dug string, nikad iznimku

Upravo to ovu klasu pogrešaka čini skupom. Grafikon se otvara, serije se iscrtavaju ispravno, osi su dobre, a jedan je natpis trendlinea jednostavno prazan. Ništa u zapisu, ništa u rukovatelju iznimki, nema dijaloga o oštećenoj datoteci. Datoteka je cijelo vrijeme bila dobra; čitač je zatražio pogrešan broj bajtova i dobio točno ono što je tražio

Zašto se BIFF8 string vraća prazan?

BIFF8 string vraća se prazan jer je zaštita duljine odbila korisni teret prije nego što je čitanje uopće počelo ili zato što se čitač zaustavio na prvom NUL-u koji je pronašao. Oba su puta po konstrukciji tiha. U HotXLS-u je zaštita obično izričita provjera DataLength u rukovatelju zapisa i mora se računati po kodiranju: korisnom teretu od 16 bitova za tijelo SXViewLink treba 8 + cch * 2 bajtova, ali 8-bitnom korisnom teretu treba samo 8 + cch. Primijenite aritmetiku širokih znakova na 8-bitni zapis i svako kratko ime pada na zaštiti. Ponašanje s NUL-om druga je zamka jer i TXLSBlob.GetString i TXLSBlob.GetWideString u dekodiranom rezultatu traže terminator i ondje skrate string, vraćajući prazan string kada terminator padne na položaj jedan. Čitajte tijelo od 16 bitova s polovicom broja bajtova i zadržat ćete prvih cch div 2 znakova; čitajte tijelo od 8 bitova kroz široki čitač i parovi bajtova stvaraju proizvoljne kodne točke. Samo je predugo čitanje glasno: TXLSBlob.EnsureReadable podiže Blob read exceeds data size kada zahtjev prijeđe blob. Prekratko čitanje nema takav alarm

GetWideString broji bajtove, a ne znakove

TXLSBlob.GetWideString(Index, Count) uzima Count u bajtovima. Iznutra radi SetString nad PWideCharom s Count div SizeOf(WideChar), pa predavanje broja znakova tiho prepolovi string. Rasporedi BIFF8 zapisa u međuvremenu duljinu stringa izražavaju u znakovima. Svaka pozivna točka za 16-bitno kodiranje zato mora sama nositi pretvorbu * 2, a svaka 8-bitna pozivna točka ne smije je nositi. To je ista granica kodiranja koja se pojavljuje kada tekst ponovno zapisujete, a ne čitate, pa vrijedi usporedno pročitati izvoz proračunskih tablica siguran za Unicode u Delphiju ako vaš cjevovod pomiče stringove u oba smjera

// 16-bitni XLUnicodeStringNoCch: cch znakova, cch * 2 bajtova
Name := Data.GetWideString(Start, cch * 2);        // ispravno
Name := Data.GetWideString(Start, cch);            // polovica teksta, bez pogreške

// 8-bitni XLUnicodeStringNoCch: cch znakova, cch bajtova
Name := WideString(Data.GetString(Start, cch));    // ispravno
Name := Data.GetWideStringWithZero(Start, cch);    // i dalje široki čitač

Konvencija vrijedi svugdje gdje se bajtni tok ručno prolazi. Kada HotXLS ponovno sastavlja dugi zapis String ($0207, [MS-XLS] 2.4.268) iz njegovih Continue zapisa ($003C), široka grana računa segCh iz duljine segmenta i zatim poziva GetWideString(3, segCh * 2) jer tijelo zapisa počinje na pomaku 3, a broj je i dalje u bajtovima. Čitač obogaćenog teksta čini istu stvar od pomaka 1 na prvom Continue segmentu. [MS-XLS] 2.5.293 jamči da granica pada na granicu dvobajtnog znaka kada je fHighByte 1, pa nije potrebno računovodstvo djelomičnih znakova, ali aritmetiku bajtova i dalje morate ispravno voditi

Što GetWideStringWithZero zapravo radi?

TXLSBlob.GetWideStringWithZero široki je čitač znakova koji zadržava ugrađene NUL-ove. Sufiks WithZero označava zadržavanje NUL-a, a ne širinu znaka: interno pokreće isti SetString nad PWideCharom s Count div SizeOf(WideChar) kao GetWideString, samo bez pretrage terminatora. Jednobajtni parnjak je TXLSBlob.GetStringWithZero, koji vraća AnsiString. Ništa u imenu ne govori koji je koji, a ta je dvosmislenost ovoj bazi koda donijela stvarne pogreške. Posebno pogrešno čitanje vrijedi imenovati jer izgleda tako uvjerljivo: GetWideString treba cch * 2, pa GetWideStringWithZero sigurno uzima cch izravno. Doista uzima cch bez prigovora, vraća WideString i kompajler je zadovoljan. Vraća i polovicu znakova, sastavljenu od pogrešnih parova bajtova. Ispravan 8-bitni put jest TXLSBlob.GetString s običnim brojem bajtova cch, pretvoren u WideString pri dodjeli. HotXLS 2.376.0 popravio je upravo tu pogrešnu upotrebu u dvama dekoderima grafikona

SXViewLink i zaštita duljine po kodiranju

SXViewLink ($0858, [MS-XLS] 2.4.316) najčišći je radni primjer jer obje asimetrije pakira u jedno zaglavlje od osam bajtova. Raspored je rt(2), unused(2), reserved(2), cch(1), fHigh(1), a zatim tijelo XLUnicodeStringNoCch: fHigh = 1 znači cch * 2 bajtova UTF-16-a, fHigh = 0 znači cch bajtova jednobajtnih znakova, a cch je ograničen na 255 jer je polje duljine jedan bajt. HotXLS zapisuje zapis u globalne vrijednosti grafikona prije Units, pokraj PivotChartBits ($0859, [MS-XLS] 2.4.196), kada list grafikona povezuje prikaz PivotTablea; pregled te mehanike na razini zapisa obrađen je u članku o zapisivanju BIFF8 PivotTable zapisa u Delphiju

// SXViewLink ([MS-XLS] 2.4.316): rt(2) unused(2) reserved(2) cch(1)
// zatim XLUnicodeStringNoCch - fHigh(1), a iza njega 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;

Dvije grane nisu kozmetika. Ranija je verzija oba kodiranja štitila izrazom širokih znakova 8 + cch * 2, pa naziv 8-bitnog prikaza koji je zapisao Excel nije prošao provjeru i dekoder je vratio prazan PivotSourceName s IsPivotChart ostavljenim na false. Veza pivota nestala je iz modela bez ijedne dijagnostike. Ista pogreška sjedila je u dekoderu Trendlinea ($2050, [MS-XLS] 2.4.328), gdje polje imena slijedi nakon 28 bajtova brojčanog tereta, s dvobajtnim cch na pomaku 28, fHigh na pomaku 30 i znakovima na pomaku 31; natpisi trendlinea koje je Excel zapisao jednobajtnim znakovima dekodirali su se u prazne stringove. Oba su popravka objavljena u istom izdanju. A 8-bitni slučaj nije povijesna zanimljivost ograničena na datoteke Excela 2.0 do 4.0: aktualni Excel i dalje zapisuje 8-bitne korisne terete BIFF8 kada svaki znak stane u jedan bajt

Kako sigurno dekodirati novi BIFF8 zapis u Delphiju?

Tamo gdje je polje doista standardni XLUnicodeString koristite TXLSBlob.GetBiffString umjesto ručnog pisanja grane. Ona čita polje duljine, čita bajt opcija, šalje ga odgovarajućem čitaču i pomiče pokazivač iza tijela. Dva Boolean parametra dio su koji treba pažljivo pročitati: is8bit opisuje širinu polja duljine, a ne širinu znakova, a iswide govori je li opcijski bajt fHigh uopće prisutan. BIFF verzije ispod $0600 nemaju ni jedno ni drugo

var
  Offset: LongWord;
begin
  Offset := 6;  // u SXViewLinku bajt cch počinje ovdje
  // is8bit = polje duljine široko je jedan bajt
  // iswide = opcijski bajt fHigh slijedi iza polja duljine
  Name := Data.GetBiffString(Offset, True, True);
  // Offset sada pokazuje na prvi bajt nakon tijela stringa

Ručne grane i dalje imaju svoje mjesto kada rukovatelj mora preživjeti skraćeni ili zlonamjerni ulaz jer se GetBiffString oslanja na to da EnsureReadable podigne iznimku, a ne na graničnu provjeru kojom vi upravljate. Zato HotXLS dekoder grafikona provjerava DataLength i ne vraća ništa umjesto da iznimku digne: neispravna radna knjiga treće strane treba vas koštati jednog natpisa, a ne cijelog dokumenta. Razmjena je namjerna i upravo zato zaštita specifična za kodiranje mora biti ispravna, jer je zaštita ono što loše čitanje pretvara u tišinu

Još jedan procesni detalj, naučen na teži način u istom izdanju. Provjeravajte spremljenu i ponovno otvorenu radnu knjigu, a ne model u memoriji koji ste upravo izgradili. Paket 2.376.0 također je otkrio SXEx emiter ([MS-XLS] 2.4.282) koji je deklarirao tijelo od 24 bajta, a zapisivao samo 22, pomičući svaki zapis nakon prikaza PivotTablea, uključujući EOF radnog lista i svaki podtok lista grafikona koji je slijedio. Postojeći testovi pivota to nikad nisu uhvatili jer su svi provjeravali memoriju. Dekodiranje stringova ima isto svojstvo: povratni prolaz kroz datoteku jedini je test koji doista vježba brojanje bajtova

Ako u Delphiju ili C++Builderu radite s internim detaljima klasičnog XLS-a i radije ne biste održavali vlastiti čitač BIFF8 zapisa, gore navedena pravila kodiranja već su implementirana i regresijski testirana u HotXLS Delphi spreadsheet component, koja čita i zapisuje XLS i XLSX bez Excela ili OLE automatizacije