HotXLS 2.376.0 je v klasičnem zapisovalniku XLS popravil zamik dolžine zapisa BIFF: oddajnik SXEx za poglede vrtilnih tabel je v glavi prijavil 24-bajtno telo, nato pa dodal 26 bajtov. Bralnik BIFF zaupa prijavljeni dolžini, zato sta odvečna bajta desinhronizirala vse pozneje, delovni zvezki, ki so vrtilno tabelo združevali z listom grafikona, pa so ob ponovnem odpiranju izgubili grafikon
Zanimiv del ni beseda »ena napaka«. Zanimiva je razdalja med napako in simptomom. Nič ni odpovedalo na mestu napake. Zapisi vrtilne tabele so se serializirali čisto, datoteka se je zapisala brez napake, Excel jo je odprl, škoda pa se je pokazala šele stotine bajtov pozneje v povsem nepovezanem podtoku. Ta razdalja je značilna za vsak binarni format s predpono dolžine in jo je vredno razumeti, preden zanj napišete nov oddajnik
Zakaj ena napačna dolžina zapisa uniči celoten tok delovnega lista?
Tok delovnega zvezka BIFF8 nima drugega uokvirjanja kot lastno aritmetiko. Vsak zapis je 4-bajtna glava z ID-jem zapisa (2 bajta) in dolžino telesa (2 bajta), ki ji sledi natanko toliko bajtov tovora ([MS-XLS] 2.1.4). Ni ločila, čarobnega bajta, kontrolne vsote ali točke za ponovno sinhronizacijo. Bralnik pride do naslednjega zapisa samo zato, ker mu je prejšnji povedal resnico o svoji velikosti. Prijavljena dolžina ni metapodatek o zapisu, temveč kazalec na naslednjega. Sledite torej, kaj sta naredila odvečna bajta. Bralnik je porabil glavo SXEx, preskočil 24 bajtov, ki jih je glava obljubila, in pristal dva bajta prezgodaj, na paru ničel, ki sta ostali iz predimenzioniranega telesa. Ti ničli je prebral kot ID zapisa $0000, nato pa je ID konca delovnega lista ($000A) prebral kot dolžino tega navideznega zapisa in vestno preskočil deset bajtov v naslednje, kar koli je prišlo. Od tam je bila vsaka glava prebrana z napačnega odmika. V delovnem zvezku z napako je to ustvarilo list grafikona, katerega _Chart je bil po ponovnem odpiranju nil, in izpis razhroščevanja, ki je $18AF razložil kot ID zapisa. Nobena od teh vrednosti se ne pojavi blizu kode vrtilne tabele
Oddajnik in zapisovalnik se nikoli ne primerjata
Strukturni razlog, da je bil zamik mogoč, je v tem, da HotXLS zapis BIFF zgradi kot TXLSBlob, katerega glava in tovor sta dve neodvisni dejstvi. EmitSXEx zapiše ID zapisa, nato Blob.AddWord(24) za dolžino, nato pa telo dodaja polje za poljem. Vrednost 24 je ročno prešteta konstanta, ki ni nikoli izpeljana iz bajtov, ki ji sledijo, in tudi ni preverjena glede nanje. Zapisovalna pot vrzeli ne zapre: AddRec blob posreduje v TXLSBlobList.Append, ta pa v izhodni tok dobesedno kopira Data.DataLength bajtov. DataLength je resnično število bajtov, zato zapisovalnik zvesto izda 26 bajtov telesa za glavo, ki trdi, da jih je 24. Obe polovici naredita natanko, kar jima je bilo naročeno, protislovje med njima pa ni nikogaršnja naloga, da ga opazi. HotXLS se temu že izogne, ko ponavlja ohranjene tovore: TXLSWorkbook.StoreDConnBlobs besedo dolžine glave izračuna iz dejanske dolžine telesa namesto iz literala, prav zato ponavljanje blobov nikoli ni zdrsnilo
Kaj [MS-XLS] 2.4.282 določa o SXEx
Specifikacija je glede velikosti nedvoumna, zato je bil popravek mehanski. [MS-XLS] 2.4.282 telo SXEx določa kot 4-bajtni grbit, ki mu sledi deset 2-bajtnih polj: csxformat, cchErrorString, cchNullString, cchTag, csxselect, crwPage, ccolPage, cchPageFieldStyle, cchTableStyle in cchVacateStyle. Štiri plus dvajset je štiriindvajset. Stari oddajnik je zapisal enajst ničelnih besed, čeprav specifikacija določa deset, klici AddWord(0) brez imen polj pa niso vsebovali informacij, zato je bilo njihovo štetje na oko pri pregledu zanesljivo toliko, kot se sliši. Predhodna dodelitev je bila namig, da je bila postavitev razumljena, zanka pa ne: TXLSBlob.Create(28) zahteva natanko štiri bajte glave in 24-bajtno telo, vendar se je blob pri vsakem klicu razširil čez ta namig in se tiho razširil, ker AdjustBufferSize po potrebi prerazporedi pomnilnik. Namig zmogljivosti, ki ga koda takoj preseže, si v vsakem serializatorju zasluži drugi pogled
function EmitSXEx(Table: TXLSPivotTable; DataList: TXLSBlobList): Integer;
var
Blob: TXLSBlob;
begin
Blob := TXLSBlob.Create(28); // 4-bajtna glava + 24-bajtno telo
Blob.AddWord($00C6);
Blob.AddWord(24);
Blob.AddByte($02);
Blob.AddByte($00); // grbit1 = fPrintTitles
Blob.AddByte($00);
Blob.AddByte($00); // grbit2
// Deset ničelnih besed dokonča 24-bajtno telo po [MS-XLS] 2.4.282
// Prijavljena dolžina se MORA ujemati z zapisanimi bajti, sicer se vsak
// zapis za tem razčleni napačno
Blob.AddWord(0); // csxformat
Blob.AddWord(0); // cchErrorString
Blob.AddWord(0); // cchNullString
Blob.AddWord(0); // cchTag
Blob.AddWord(0); // csxselect
Blob.AddWord(0); // crwPage
Blob.AddWord(0); // ccolPage
Blob.AddWord(0); // cchPageFieldStyle
Blob.AddWord(0); // cchTableStyle
Blob.AddWord(0); // cchVacateStyle
AddRec(DataList, Blob);
Result := 1;
end;
Kako je to preživelo celotno testno zbirko vrtilnih tabel?
Ker obstoječi testi vrtilnih tabel niso nikoli izvedli povratnega zapisa skozi datoteko. Zgradili so delovni zvezek, preverili model v pomnilniku in se tam ustavili, preverjanja v pomnilniku pa ne morejo videti neskladja dolžine, ki obstaja samo v serializiranem bajtnem toku. Nabor zapisov, ki ga pokriva pisanje zapisov vrtilne tabele BIFF8 iz Delphija, je bil po tem merilu dobro preizkušen, vendar je vseeno izdal oddajnik, ki kvari tok. Napaka je potrebovala še drugo funkcijo, da je postala vidna: vrtilni delovni list, ki mu ni sledilo veliko, se je še vedno znova odprl, ker je korupcija odtekla čez konec podtoka, ki ga nihče ni pregledal. Šele kombinacija vrtilne tabele in lista grafikona, kjer listi grafikonov in risbe zasedajo podtok za delovnim listom, je tiho napačno poravnavo spremenila v vidno manjkajoč objekt
// PivotChartRoundTripThroughLinkRecords, skrajšano
Wb.Sheets.Add.Name := 'Report';
Wb.Sheets[2].AddPivotTable('Data!A1:B3', 2, 2, 'SalesPivot');
Wb.Sheets.AddChartSheet('PivotView', TXLSChartType(2), '', '', '',
Series, No3D, PivotInfo);
Assert.AreEqual(1, Wb.SaveAs(TempPath));
Wb.Free;
Wb := TXLSWorkbook.Create;
Wb.Open(TempPath); // napačno razčlenjevanje se zgodi tukaj
Model := Wb.Sheets[3]._Chart.GetChartModel;
Assert.IsTrue(Model.IsPivotChart);
Pred popravkom je bil Wb.Sheets[3]._Chart v tej vrstici nil, ker je bralnik mejo podtoka izgubil že dolgo pred BOF grafikona. Preverjanje, ki je končno ujelo napako pri serializaciji vrtilne tabele, je bilo preverjanje grafikona
Kako napačno poravnan tok BIFF prebrati nazaj do prvega slabega zapisa
Sprehodite se po verigi glav in jo izpišite, saj napačno sinhroniziran tok BIFF svojo strukturno napako napove precej prej, preden so podatki videti napačni. Začnite pri BOF podtoka ($0809), preberite ID in dolžino, napredujte za štiri plus dolžino in ponavljajte. Dokler je tok poravnan, pristanete na verjetnih ID-jih zapisov in veriga se konča natanko pri EOF ($000A). Ko zdrsne, dobite ID-je, ki ne obstajajo, dolžine, ki pretečejo čez medpomnilnik, ali verigo, ki gre naravnost mimo mesta, kjer bi moral biti EOF
// Sprehod po toku zapisov BIFF in ustavitev pri prvi glavi, ki ne more biti prava
procedure ScanRecords(Buf: PByte; Size: LongWord);
var
Pos: LongWord;
Id, Len: Word;
begin
Pos := 0;
while Pos + 4 <= Size do
begin
Id := PWord(Buf + Pos)^;
Len := PWord(Buf + Pos + 2)^;
// Ničelni ID ni nikoli veljaven zapis, telo, ki sega čez
// medpomnilnik, pa dokazuje, da je veriga prej že zdrsnila
if (Id = 0) or (Pos + 4 + LongWord(Len) > Size) then
begin
WriteLn(Format('desync at %d: id=$%.4x len=%d', [Pos, Id, Len]));
Break;
end;
WriteLn(Format('%6d id=$%.4x len=%d', [Pos, Id, Len]));
if Id = $000A then
WriteLn('-- EOF, substream ends cleanly --');
Inc(Pos, 4 + LongWord(Len));
end;
end;
Nato izhod preberite od zadaj in si zapomnite eno pravilo: prvi zapis, ki se ne razčleni, skoraj nikoli ni krivec. Je žrtev. Krivec je zapis tik pred njim, zadnji, ki se je razčlenil brez pritožbe, saj lažnivec o lastni dolžini vedno uspešno razčleni sebe. V tem primeru se je sprehod ustavil pri navideznem zapisu $0000, zapis pred njim pa je bil SXEx. Primerjajte prijavljeno dolžino tega zapisa s seznamom polj v specifikaciji, bajt za bajtom, in aritmetika se bodisi sešteje bodisi ne. Če sprehod sploh ne pride do razumnega prvega zapisa, je težava plast nižje, v sestavljeni datoteki OLE2, ki vsebuje tok Workbook, in nobeno izpisovanje na ravni zapisov ne bo pomagalo
Oddajnik, ki ne more lagati o lastni dolžini
Trajni popravek ni pravilna konstanta, temveč odstranitev možnosti za zapis napačne. Rezervirajte besedo dolžine, oddajte telo, nato glavo popravite iz števila bajtov, ki ste jih dejansko ustvarili. HotXLS izpostavi, kar za to potrebujete: TXLSBlob.DataLength da trenutni odmik, SetWord pa zapiše nazaj na že oddano mesto
function BeginRecord(Blob: TXLSBlob; RecId: Word): LongWord;
begin
Blob.AddWord(RecId);
Result := Blob.DataLength; // zapomni si, kje je beseda dolžine
Blob.AddWord(0); // nadomestno mesto, ki ga popravi EndRecord
end;
procedure EndRecord(Blob: TXLSBlob; LenPos: LongWord);
var
Body: LongWord;
begin
Body := Blob.DataLength - LenPos - SizeOf(Word);
if Body > 8224 then
raise Exception.Create('BIFF body exceeds 8224 bytes, split with Continue');
Blob.SetWord(Word(Body), LenPos);
end;
Pošteno povejte, kje se to zagotovilo konča. Splošna trditev, da so oddani bajti enaki 2 + 2 + prijavljeni vrednosti, velja samo za zapise, ki ostanejo pod omejitvijo BIFF8, 8224 bajtov tovora. Predimenzionirana telesa upravičeno v glavi prijavijo 8224 in nadaljujejo v zapisih Continue $003C, kar počneta prav zapisovalnika predpomnilnika vrtilne tabele in povezav HotXLS pri velikih tovorih, zato je invariant pogojen: pod mejo mora biti dolžina oddanega bloba enaka prijavljeni dolžini plus štiri, nad njo pa aritmetiko prevzame razdelilnik. To razliko zapišite v pomočnik, ne v komentar. Isto razmišljanje velja za vsak format tag-length-value, ne samo za BIFF. Oddajnik, ki prijavi velikost, preden jo pozna, poda trditev, ki je koda ne more preveriti in pregledovalec prešteti, deluje pa vse do trenutka, ko druga funkcija pristane za prvo
Zapisovalnik BIFF8, oddajniki zapisov vrtilne tabele in podtok grafikona, obravnavan tukaj, so del komponente HotXLS Delphi spreadsheet za Delphi in C++Builder, ki bere in zapisuje XLS, XLSX in ODS brez nameščenega Excela