HotXLS 2.376.0, klasik XLS writer'ındaki bir BIFF record length kaymasını düzeltti: PivotTable view'ları için SXEx emitter header'ında 24 baytlık bir body bildiriyor, ardından 26 bayt ekliyordu. Bir BIFF reader bildirilen uzunluğa güvenir; bu nedenle iki fazla bayt aşağı akıştaki her şeyi senkronizasyondan çıkardı ve PivotTable ile chart sheet'i eşleştiren workbook'larda yeniden açılışta chart kayboldu
İlginç olan off-by-one kelimesi değil, hata ile belirti arasındaki mesafedir. Bug noktasında hiçbir şey başarısız olmadı. Pivot record'ları temiz biçimde serialize edildi, dosya hatasız yazıldı, Excel dosyayı açtı ve hasar ancak yüzlerce bayt aşağıda, bütünüyle ilgisiz bir substream'de ortaya çıktı. Bu mesafe length-prefixed her binary formatın karakteristiğidir ve bir tane için yeniden emitter yazmadan önce anlaşılmaya değer
Tek bir yanlış record length neden bütün worksheet stream'ini yok eder?
Bir BIFF8 workbook stream'inin kendi aritmetiği dışında framing'i yoktur. Her record, record id'sinden (2 bayt) ve body length'ten (2 bayt) oluşan 4 baytlık bir header'ın ardından tam olarak o kadar payload baytı taşır ([MS-XLS] 2.1.4). Separator, magic byte, checksum ve resynchronization point yoktur. Reader bir sonraki record'a ancak önceki record kendi boyutu hakkında doğruyu söylediği için ulaşır. Declared length record hakkında metadata değildir; bir sonraki record'a giden pointer'dır. İki fazla baytın ne yaptığına bakın. Reader SXEx header'ını tüketti, header'ın vaat ettiği 24 baytı atladı ve oversized body'den kalan iki sıfır baytın üzerine, iki bayt erken indi. Bu sıfırları $0000 record id'si olarak okudu, ardından worksheet EOF record id'sini ($000A) o phantom record'ın length'i olarak okudu ve önündeki ne varsa on bayt içine sadakatle atladı. Bundan sonra her header yanlış ofsette okundu. Başarısız workbook'ta bu, yeniden açıldıktan sonra _Chart'ı nil olan bir chart sheet ve $18AF'nin record id olarak yorumlandığını gösteren bir debug dump üretti. Bu değerlerin hiçbiri pivot kodunun yakınında bile görünmez
Emitter ile writer neden birbirinden habersiz?
Kaymanın mümkün olmasının yapısal nedeni, HotXLS'in bir BIFF record'ını header ile payload'ı iki bağımsız gerçek olan bir TXLSBlob olarak kurmasıdır. EmitSXEx record id'yi, ardından length için Blob.AddWord(24)'ü yazar, sonra body'yi alan alan ekler. Bu 24 elle sayılmış bir sabittir; arkasından gelen baytlarla türetilmez ve karşılaştırılmaz. Write path de boşluğu kapatmaz: AddRec blob'ı TXLSBlobList.Append'e iletir, o da Data.DataLength baytını çıktıya aynen kopyalar. DataLength gerçek bayt sayısıdır; writer header 24 derken body'nin 26 baytını sadakatle yayımlar. İki yarı da kendilerine söyleneni tam yapar ve aralarındaki çelişkiyi fark etmek kimsenin işi değildir. HotXLS, korunan payload'ları yeniden oynattığı yerde bu hatadan zaten kaçınır: TXLSWorkbook.StoreDConnBlobs header length word'ünü literal yerine gerçek body length'ten hesaplar; blob replay'in şimdiye kadar hiç kaymamasının nedeni budur
[MS-XLS] 2.4.282 SXEx hakkında neyi kesinleştirir?
Specification boyut konusunda belirsiz değildir; bu da düzeltmeyi mekanik hale getirir. [MS-XLS] 2.4.282, SXEx body'sini 4 baytlık bir grbit ile ardından on adet 2 baytlık alan olarak tanımlar: csxformat, cchErrorString, cchNullString, cchTag, csxselect, crwPage, ccolPage, cchPageFieldStyle, cchTableStyle ve cchVacateStyle. Dört artı yirmi yirmi dörttür. Eski emitter specification on alan tanımlarken on bir zero word yazıyordu ve anonymous AddWord(0) çağrıları hiçbir field name taşımıyordu; bu nedenle review sırasında onları gözle saymak kulağa geldiği kadar güvenilirdi. Ön allocation ipucuydu: TXLSBlob.Create(28) tam olarak dört header baytı artı 24 baytlık body ister, ama blob her çağrıda bu ipucunu aşarak sessizce büyüyordu; çünkü AdjustBufferSize gerektiğinde yeniden allocate eder. Kodun hemen aştığı bir capacity hint, her serializer'da ikinci kez bakmayı hak eder
function EmitSXEx(Table: TXLSPivotTable; DataList: TXLSBlobList): Integer;
var
Blob: TXLSBlob;
begin
Blob := TXLSBlob.Create(28); // 4 baytlık header + 24 baytlık body
Blob.AddWord($00C6);
Blob.AddWord(24);
Blob.AddByte($02);
Blob.AddByte($00); // grbit1 = fPrintTitles
Blob.AddByte($00);
Blob.AddByte($00); // grbit2
// [MS-XLS] 2.4.282'ye göre on zero word 24 baytlık body'yi tamamlar
// Bildirilen uzunluk yazılan baytlarla EŞLEŞMELİ, yoksa bu kayıttan
// sonraki her record yanlış ayrıştırılır
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;
Bu hata bütün bir PivotTable test paketinden nasıl sağ çıktı?
Çünkü mevcut pivot testleri hiçbir zaman bir dosya üzerinden round-trip yapmadı. Bir workbook kurdular, in-memory model'i doğruladılar ve durdular; memory assertion'ları yalnızca serialize edilmiş byte stream'de bulunan bir length mismatch'i göremez. Delphi'den BIFF8 PivotTable record'ları yazma tarafından kapsanan record set'i bu standarda göre iyi test edilmişti ve yine de stream'i bozan bir emitter yayımlandı. Kusurun görünür olması için ikinci bir feature da gerekiyordu: arkasında fazla bir şey olmayan pivot'lu bir worksheet yine açılır, çünkü corruption kimsenin incelemediği bir substream'in sonuna taşardı. Yalnızca PivotTable ile chart sheet'in birleşimi, chart sheet ve drawing'lerin worksheet'ten sonra gelen bir substream'i işgal ettiği durumda, sessiz hizasızlığı gözle görünür eksik bir nesneye çevirdi
// PivotChartRoundTripThroughLinkRecords, kısaltılmış
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); // yanlış ayrıştırma burada gerçekleşir
Model := Wb.Sheets[3]._Chart.GetChartModel;
Assert.IsTrue(Model.IsPivotChart);
Düzeltmeden önce o satırda Wb.Sheets[3]._Chart nil'di; çünkü reader chart BOF'a ulaşmadan çok önce substream sınırını kaybetmişti. Bir pivot serialization bug'ını sonunda yakalayan assertion chart hakkında bir assertion'dı
Hizasız BIFF stream'i ilk bozuk record'a kadar nasıl geri okunur?
Header zincirini yürüyüp yazdırın, çünkü senkronizasyondan çıkmış BIFF stream'i data yanlış görünmeden çok önce kendini yapısal olarak belli eder. Substream BOF ($0809) ile başlayın, id ve length'i okuyun, dört artı length kadar ilerleyin ve tekrarlayın. Stream hizalıyken makul record id'lerine inersiniz ve zincir tam olarak EOF'da ($000A) biter. Kaydığında var olmayan id'ler, buffer'ı aşan length'ler veya EOF olması gereken yeri aşarak yürüyen bir zincir görürsünüz
// Bir BIFF record stream'ini yürü ve gerçek olamayacak ilk header'da dur
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)^;
// Sıfır id hiçbir zaman geçerli bir record değildir; buffer'ın dışına
// taşan bir body zincirin yukarıda zaten kaydığının kanıtıdır
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;
Ardından çıktıyı geriye doğru okuyun ve şu kuralı aklınızda tutun: ayrıştırılamayan ilk record neredeyse hiçbir zaman suçlu değildir. Kurbandır. Suçlu, şikâyet etmeden ayrıştırılan son record, yani hemen öncesindeki record'dur; kendi length'i hakkında yalan söyleyen bir record her zaman sorunsuz ayrıştırılır. Bu durumda yürüyüş phantom $0000 record'ında durdu ve ondan önceki record SXEx'ti. O record'un declared length'ini specification'daki field list ile bayt bayt karşılaştırın; aritmetik ya tutar ya tutmaz. Yürüyüş makul bir ilk record'a bile ulaşmıyorsa sorun bir alt katmandadır: Workbook stream'ini taşıyan OLE2 compound file ve hiçbir record-level dump yardımcı olmaz
Kendi length'i hakkında yalan söyleyemeyen bir emitter
Kalıcı çözüm doğru bir sabit yazmak değil, yanlış sabit yazma fırsatını kaldırmaktır. Length word için yer ayırın, body'yi emit edin, sonra header'ı gerçekten ürettiğiniz byte count'tan patch edin. HotXLS bunun için gerekeni sunar: TXLSBlob.DataLength mevcut ofseti verir ve SetWord daha önce emit edilmiş bir konuma geri yazar
function BeginRecord(Blob: TXLSBlob; RecId: Word): LongWord;
begin
Blob.AddWord(RecId);
Result := Blob.DataLength; // length word'ün durduğu yeri hatırla
Blob.AddWord(0); // EndRecord tarafından patch edilecek placeholder
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;
Bu garantinin nerede bittiği konusunda dürüst olun. Emit edilen baytların 2 + 2 + declared'a eşit olduğunu söyleyen genel bir assertion yalnızca BIFF8'in 8224 payload baytlık sınırının altında kalan record'lar için geçerlidir. Büyük body'ler header'da meşru olarak 8224 bildirir ve $003C Continue record'larıyla devam eder; HotXLS pivot cache ve connection writer'larının büyük payload'lar için yaptığı tam olarak budur. Dolayısıyla invariant koşulludur: sınırın altında emit edilen blob length declared length artı dört olmalı, üstünde arithmetic'i splitter sahiplenmelidir. Bu ayrımı comment'e değil helper'a kodlayın. Aynı düşünce yalnızca BIFF'e değil her tag-length-value formatına taşınır. Boyutu bilmeden declare eden bir emitter, kodun kontrol edemediği ve review yapanın sayamadığı bir iddia yazar; ilk özelliğin aşağı akışına ikinci bir feature gelene kadar çalışır
Burada anlatılan BIFF8 writer, pivot record emitter'ları ve chart substream, Delphi ve C++Builder için HotXLS Delphi spreadsheet component'in parçasıdır; Excel kurulu olmadan XLS, XLSX ve ODS okur ve yazar