Techninis straipsnis

BIFF įrašo ilgio neatitikimas Delphi XLS rašyklėje

HotXLS 2.376.0 ištaisė klasikinės XLS rašyklės BIFF įrašo ilgio neatitikimą: PivotTable rodinių SXEx generatorius antraštėje deklaravo 24 baitų kūną, o tada pridėdavo 26 baitus. BIFF skaitytuvas pasitiki deklaruotu ilgiu, todėl du pertekliniai baitai išderino visą tolesnį srautą, ir darbaknygės, kuriose PivotTable buvo suporuota su diagramos lapu, vėl atvėrus prarasdavo diagramą

Įdomu ne vienas per daug įrašytas žodis. Įdomus atstumas tarp klaidos ir požymio. Klaidos vietoje niekas nesugedo. Pivot įrašai buvo serializuoti švariai, failas įrašytas be klaidos, Excel jį atidarė, o žala išryškėjo tik šimtais baitų toliau, visiškai nesusijusiame posrautyje. Toks atstumas būdingas kiekvienam ilgiu pagrįstam dvejetainiam formatui, todėl verta jį suprasti prieš rašant dar vieną generatorių

Kodėl vienas neteisingas įrašo ilgis sunaikina visą darbalapio srautą?

BIFF8 darbaknygės srautas neturi kito kadravimo, tik savo aritmetiką. Kiekvienas įrašas turi 4 baitų antraštę: įrašo ID (2 baitai) ir kūno ilgį (2 baitai), po jos eina tiksliai tiek naudingosios apkrovos baitų, kiek nurodyta ([MS-XLS] 2.1.4). Nėra skyriklio, magiško baito, kontrolinės sumos ar persisinchronizavimo taško. Skaitytuvas ant kito įrašo nusileidžia tik todėl, kad ankstesnis įrašas teisingai pasakė savo dydį. Deklaruotas ilgis nėra įrašo metaduomuo, tai rodyklė į kitą įrašą. Atsekite, ką padarė du pertekliniai baitai. Skaitytuvas perskaitė SXEx antraštę, praleido 24 antraštės pažadėtus baitus ir dviem baitais per anksti atsidūrė ant per didelio kūno paliktos nulių poros. Tuos nulius jis perskaitė kaip įrašo ID $0000, po to darbalapio EOF įrašo ID ($000A) perskaitė kaip tariamo įrašo ilgį ir pareigingai praleido dešimt baitų į tai, kas buvo toliau. Nuo tos vietos kiekviena antraštė perskaityta ne tuo poslinkiu. Sugedusioje darbaknygėje tai sukūrė diagramos lapą, kurio _Chart po atvėrimo buvo nil, ir derinimo išklotinę, kurioje $18AF interpretuotas kaip įrašo ID. Nė viena iš šių reikšmių nėra šalia pivot kodo

Generatorius ir rašyklė niekada nepalygina užrašų

Struktūrinė priežastis, leidusi atsirasti neatitikimui, yra tai, kad HotXLS BIFF įrašą sukuria kaip TXLSBlob, kurio antraštė ir naudingoji apkrova yra du nepriklausomi faktai. EmitSXEx įrašo įrašo ID, tada Blob.AddWord(24) įrašo ilgį, o po to kūną prideda lauką po lauko. Tas 24 yra ranka suskaičiuota konstanta, niekada nevedama iš toliau einančių baitų ir su jais netikrinama. Rašymo kelias spragos taip pat neuždaro: AddRec perduoda blobą TXLSBlobList.Append, kuri į išvesties srautą pažodžiui nukopijuoja Data.DataLength baitų. DataLength yra tikrasis baitų skaičius, todėl rašyklė ištikimai išveda 26 kūno baitus už antraštės, teigiančios, kad jų yra 24. Abi pusės daro tiksliai tai, kas liepta, o prieštaravimas tarp jų yra niekieno pareiga jį pastebėti. HotXLS jau išvengia šios problemos atkurdama išsaugotą naudingąją apkrovą: TXLSWorkbook.StoreDConnBlobs antraštės ilgio žodį apskaičiuoja iš tikrojo kūno ilgio, todėl blobų atkūrimas niekada nenukrypo

Ką apie SXEx nustato [MS-XLS] 2.4.282

Specifikacija nedviprasmiškai nustato dydį, todėl pataisymas buvo mechaninis. [MS-XLS] 2.4.282 SXEx kūną apibrėžia kaip 4 baitų grbit ir dešimt 2 baitų laukų: csxformat, cchErrorString, cchNullString, cchTag, csxselect, crwPage, ccolPage, cchPageFieldStyle, cchTableStyle ir cchVacateStyle. Keturi plius dvidešimt yra dvidešimt keturi. Senasis generatorius pagal specifikaciją turėjo įrašyti dešimt nulinių žodžių, bet įrašė vienuolika, o anoniminiai AddWord(0) iškvietimai neturėjo laukų vardų, todėl jų skaičiavimas akimis per peržiūrą buvo toks pat patikimas, kaip skamba. Išankstinio paskirstymo užuomina buvo pėdsakas, kad maketas suprastas, o ciklas – ne: TXLSBlob.Create(28) prašo lygiai keturių antraštės ir 24 kūno baitų, bet blobas kiekvieno iškvietimo metu peržengdavo šią užuominą ir augdavo tyliai, nes AdjustBufferSize prireikus perskirsto atmintį. Į talpos užuominą, kurią kodas iškart viršija, verta dar kartą pažiūrėti bet kuriame serializatoriuje

function EmitSXEx(Table: TXLSPivotTable; DataList: TXLSBlobList): Integer;
var
  Blob: TXLSBlob;
begin
  Blob := TXLSBlob.Create(28);   // 4 baitų antraštė + 24 baitų kūnas
  Blob.AddWord($00C6);
  Blob.AddWord(24);
  Blob.AddByte($02);
  Blob.AddByte($00);             // grbit1 = fPrintTitles
  Blob.AddByte($00);
  Blob.AddByte($00);             // grbit2
  // Dešimt nulinių žodžių užbaigia 24 baitų kūną pagal [MS-XLS] 2.4.282
  // Deklaruotas ilgis PRIVALO sutapti su įrašytais baitais, kitaip
  // kiekvienas po šio įrašo einantis įrašas bus analizuojamas neteisingai
  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;

Kodėl tai išgyveno visą PivotTable testų rinkinį?

Nes esami pivot testai niekada neatliko kelionės per failą pirmyn ir atgal. Jie sukurdavo darbaknygę, tikrindavo atminties modelį ir sustodavo, o atminties tvirtinimai nemato ilgio neatitikimo, kuris egzistuoja tik serializuotame baitų sraute. Įrašų rinkinys, aprašytas rašant BIFF8 PivotTable įrašus Delphi aplinkoje, pagal tokį standartą buvo gerai patikrintas, tačiau vis tiek išsiuntė srautą gadinantį generatorių. Defektui taip pat reikėjo antros funkcijos, kad jis taptų matomas: pivotuotas darbalapis, po kurio nieko svarbaus nebuvo, vis dar atsiverdavo, nes pažeidimas nuėjo už posraučio galo, kurio niekas netikrino. Tik PivotTable ir diagramos lapo derinys, kai diagramų lapai ir piešiniai užima po darbalapio einantį posrautį, tylų poslinkį pavertė aiškiai dingusiu objektu

// PivotChartRoundTripThroughLinkRecords, sutrumpinta
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);                       // čia įvyksta neteisingas išanalizavimas
Model := Wb.Sheets[3]._Chart.GetChartModel;
Assert.IsTrue(Model.IsPivotChart);

Prieš pataisymą Wb.Sheets[3]._Chart toje eilutėje buvo nil, nes skaitytuvas dar gerokai prieš pasiekdamas diagramos BOF buvo praradęs posraučio ribą. Tvirtinimas, kuris pagaliau aptiko pivot serializavimo klaidą, buvo tvirtinimas apie diagramą

Kaip iš neteisingai sulygiuoto BIFF srauto grįžti prie pirmo blogo įrašo

Apvaikščiokite antraščių grandinę ir ją išspausdinkite, nes išsiderinęs BIFF srautas struktūriškai apie save praneša gerokai anksčiau, nei duomenys atrodo neteisingi. Pradėkite posraučio BOF ($0809), perskaitykite ID ir ilgį, pasislinkite keturiais plius ilgiu baitais ir kartokite. Kol srautas sulygiuotas, atsidursite ties tikėtinais įrašų ID, o grandinė tiksliai baigsis EOF ($000A). Kai ji nukrypsta, gaunate neegzistuojančius ID, buferį viršijančius ilgius arba grandinę, einančią tiesiai pro vietą, kur turėjo būti EOF

// Pereiname BIFF įrašų srautą ir sustojame ties pirmąja antrašte, kuri negali būti tikra
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)^;
    // Nulinis ID niekada nėra teisėtas įrašas, o kūnas, išeinantis už
    // buferio ribų, įrodo, kad grandinė jau kažkur anksčiau nukrypo
    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;

Tada skaitykite išvestį atgal ir laikykitės vienos taisyklės: pirmas įrašas, kurio nepavyksta išanalizuoti, beveik niekada nėra kaltininkas. Jis yra auka. Kaltininkas yra prieš jį esantis įrašas, paskutinis, kuris buvo išanalizuotas be skundo, nes melagis apie savo paties ilgį visada išanalizuojamas sėkmingai. Šiuo atveju eiga sustojo ties tariamu $0000 įrašu, o prieš jį buvo SXEx. Palyginkite to įrašo deklaruotą ilgį su specifikacijoje pateiktu laukų sąrašu baitas po baito ir aritmetika arba sutaps, arba ne. Jei eiga nepasiekia net protingo pirmo įrašo, problema yra žemesniame sluoksnyje – OLE2 sudėtiniame faile, laikančiame Workbook srautą, ir joks įrašų lygio išklotinės kiekis nepadės

Generatorius, kuris negali meluoti apie savo ilgį

Ilgalaikis pataisymas nėra teisinga konstanta, o galimybės įrašyti neteisingą konstantą pašalinimas. Rezervuokite ilgio žodį, išveskite kūną, tada antraštę pataisykite pagal faktiškai sukurtą baitų skaičių. HotXLS pateikia tam reikalingus įrankius: TXLSBlob.DataLength nurodo dabartinį poslinkį, o SetWord įrašo reikšmę atgal į jau išvestą vietą

function BeginRecord(Blob: TXLSBlob; RecId: Word): LongWord;
begin
  Blob.AddWord(RecId);
  Result := Blob.DataLength;   // įsimename ilgio žodžio vietą
  Blob.AddWord(0);             // vieta, kurią pataisys 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;

Sąžiningai įvardykite, kur ši garantija baigiasi. Bendras tvirtinimas, kad išvesti baitai lygūs 2 + 2 + deklaruotam ilgiui, tinka tik įrašams, telpantiems į BIFF8 8224 naudingosios apkrovos baitų ribą. Per dideli kūnai teisėtai antraštėje deklaruoja 8224 ir tęsiasi $003C Continue įrašuose, būtent taip HotXLS pivotų podėlio ir ryšių rašyklės tvarko dideles apkrovas, todėl invariantas sąlyginis: žemiau ribos išvesties bloko ilgis turi būti lygus deklaruotam ilgiui plius keturi, o virš jos aritmetiką valdo skaidytuvas. Įrašykite šį skirtumą į pagalbinę funkciją, o ne komentarą. Tas pats samprotavimas perkeltas į kiekvieną tag-length-value formatą, ne tik BIFF. Generatorius, kuris dydį deklaruoja prieš jį sužinodamas, pateikia teiginį, kurio kodas negali patikrinti, o peržiūrėtojas – suskaičiuoti, ir jis veikia iki tol, kol pirmo įrašo tolesnėje grandyje atsiranda antra funkcija

BIFF8 rašyklė, pivot įrašų generatoriai ir čia aptartas diagramos posrautas pateikiami HotXLS Delphi skaičiuoklių komponente, skirtame Delphi ir C++Builder, kuris skaito ir rašo XLS, XLSX bei ODS be įdiegto Excel