Substream BIFF PivotCache trzyma zbuforowany zbiór danych tabeli przestawnej osobno od widoku, który go wyświetla, a HotXLS czyta i zapisuje ten substream, oglądając ciała rekordów, zamiast ufać numerom rekordów. To rozróżnienie to cała historia: ten sam numer rekordu wozi dwa niekompatybilne layouty ciała, zależnie od tego, który writer wyprodukował plik, więc reader rozstrzyga ramowanie po pierwszym ciele rekordu, jakie zobaczy
Tą warstwą zderzasz się w chwili, gdy tabela przestawna ma przeżyć rundę w obie strony. Widok przestawny bez swojego cache to muszla, a Excel odbuduje cache z zakresu źródłowego przy otwarciu pliku, co jest w porządku dokładnie do momentu, w którym zakresu źródłowego już nie ma, dane zostały wklejone z zapytania albo skoroszyt to zarchiwizowane zamknięcie, którego nie wolno zmienić, gdy ktoś go otworzy
Dwie struktury, dwa miejsca w pliku
Zbuforowane dane i definicja cache mieszkają w różnych częściach skoroszytu i pomieszanie ich to pierwsza rzecz do ogarnięcia. Zbuforowane rekordy tworzą własny substream, podany w [MS-XLS] §2.1.7.12 jako PIVOTCACHE = SXDB SXDBEx *SXFORMULA *FDB *DBB EOF. Zauważ, czego brakuje: nie ma BOF na czele tej produkcji
Definicja siedzi za to w globals skoroszytu, jako PIVOTCACHEDEFINITION = SXStreamID SXVS [SXSRC] [SXADDLCACHE] (§2.1.7.20.3), umieszczona po rekordach formatowania i przed rekordami BoundSheet i Country. Pojedynczy cache jest więc opisany w dwóch miejscach oddalonych o setki rekordów, a łącznikiem między nimi jest identyfikator strumienia, który musi zgadzać się w trzech lokalizacjach naraz
Każdy cache należy do strumienia pod _SX_DB_CUR, którego nazwą jest czterocyfrowy, wielkimi literami zapis heksadecymalny jego identyfikatora. SXStreamID.idStm, pole idstm powtórzone w nagłówku SXDB i ta nazwa strumienia muszą wszystkie pasować. Alokując nowy identyfikator, zarezerwuj najpierw każdy numer już odczytany z pliku, inaczej nowy cache może zagarnąć numer należący do starszego cache, do którego reader jeszcze nie dotrwał
Jeszcze jeden identyfikator łapie ludzi. Wartość iCache w widoku przestawnym to pozycja od zera odpowiadającego SXStreamID w globalnej sekwencji, a nie identyfikator cache, który możesz wybrać. Przy zapisie musi być mapowana z obiektu cache na jego faktyczną pozycję wyjściową, a istniejące widoki muszą zostać przenumerowane razem z nią, inaczej upgrade jednego cache po cichu celuje widok w inny
var
Book: TXLSWorkbook;
Cache: TXLSPivotCache;
Field: TXLSPivotCacheField;
V: TXLSPivotCacheValue;
begin
Book := TXLSWorkbook.Create(nil);
try
Book.LoadFromFile('sales.xls');
Cache := Book.PivotCaches.Add;
Cache.SourceRangeSheet := 'Data';
Cache.SourceFirstRow := 1; Cache.SourceFirstCol := 1;
Cache.SourceLastRow := 500; Cache.SourceLastCol := 6;
Cache.SourceDataType := 1; // SXVS SHEET, MS-XLS 2.4.317
Cache.RefreshOnLoad := False; // ufaj zbuforowanym rekordom
Cache.SaveData := True;
Field := Cache.AddField('Region', xlpcftString);
V.ValueType := xlpcftString;
V.StrValue := 'North';
Field.FindOrAddItem(V);
Cache.SetRecordCount(0); // wyczyść, potem zwymiaruj siatkę rekordów
Cache.SetRecordCount(500);
Book.StorePivotCaches;
finally
Book.Free;
end;
end;
Podwójne SetRecordCount to nie zabobon. RecordCount to zwykły zapis właściwości, który nic nie alokuje, a wewnętrzna ścieżka wzrostu inicjalizuje tylko nowo dodane wiersze, więc cache, którego licznik został ustawiony ścieżką nagłówkową, może skończyć z siatką indeksów o zerowej długości. Zapisy do RecordIndices są wtedy wyrzucane bez błędu. Ustawienie licznika na zero i z powrotem odbudowuje siatkę i musi się zdarzyć po dodaniu każdego pola, bo szerokość wiersza bierze się z liczby pól
Dlaczego numer rekordu nie umie ci powiedzieć layoutu ciała?
Bo numery rekordów i layouty ciała zmieniały się w różnym czasie, więc mapowanie między nimi nie jest funkcją. Jeden numer w zestawie legacy pojawia się wyłącznie w plikach starszych writerów, co czyni go pewnym sygnałem w jednym kierunku. Inny numer jest naprawdę dwuznaczny: występuje i w poprawnych plikach, i w szeregu wersji pośrednich, które używały nowego numeru ze starym layoutem ciała
Ramowanie trzeba więc rozstrzygać z ciała, i to raz na substream cache, a nie per rekord. HotXLS rozstrzyga dialekt z długości pierwszego rekordu SXDBB w każdym substreamie. W ramowaniu specyfikacji jeden SXDBB trzyma dokładnie jeden rekord cache, więc jego długość równa się szerokości jednego wiersza. W starszym ramowaniu pakowanym pierwszy rekord trzyma tyle wierszy, ile się zmieści, więc dla każdego cache o więcej niż jednym wierszu ma co najmniej dwie szerokości wiersza. Porównanie jest rozstrzygające zawsze, gdy dwie prognozy się różnią
Gdy się nie różnią, reader bierze odczyt specyfikacji, z zasady, że pliki napisane przez Excela liczebnie biją pliki napisane przez build pośredni. Ta ślepa plama jest wąska z konstrukcji, a gdy już się zdarzy, plik sam w sobie wciąż odtwarza się bajt w bajt. Dotknięte są tylko typowane indeksy wystawiane wołającym
Szerokość indeksu mieszka w innym rekordzie
SXDBB (§2.4.276) wozi jeden indeks na pole cache z ustawioną flagą wartości wyróżnionych, w kolejności pól, a szerokość każdego indeksu rozstrzyga się gdzie indziej: odpowiadający rekord pola SXFDB (§2.4.283) deklaruje flagę short-items, a ta flaga mówi, czy indeks zajmuje dwa bajty, czy jeden. Dwa rekordy, jeden dorozumiany kontrakt i jedno zdanie w specyfikacji, które je łączy
To sprzężenie to dokładnie miejsce, w którym domowy encoding się wywraca. Wcześniejszy writer HotXLS pakietyzował każde pole do minimalnej liczby bitów, dopełniając do granicy bajtu między wierszami, co w izolacji da się obronić i bezpośrednio przeczy szerokości, którą ten sam writer właśnie zadeklarował w SXFDB. Pole o trzech wartościach wyróżnionych było opisane jako szerokie na jeden bajt w jednym rekordzie i zajmowało dwa bity w drugim. Naprawa nie polegała na poprawieniu arytmetyki, tylko na wyniesieniu decyzji o szerokości do jednej funkcji, którą wołają oba emitery, więc dwa rekordy nie mogą się już rozjechać. To ta sama klasa defektu opisana w artykule dryf deklaracji długości rekordu BIFF, gdzie zadeklarowany rozmiar i rzeczywiste ciało rozstają się
Konsekwencję całkowitego nieczytania tych rekordów warto wyartykułować, bo łatwo ją nie docenić. Gdy reader pomijał indeksy rekordów, każdy cache załadowany z pliku zgłaszał indeks zero dla każdego pola każdego wiersza, co znaczy, że każdy wiersz celował w pierwszą wartość każdego pola. To nie jest tylko okrojona introspekcja: ścieżka ewaluacji przestawnej i ścieżka wypełniania komórek z cache obie konsumują tę siatkę. A test rundy w obie strony nie wykryje tego, bo cache wciąż na surowym replayu jest zapisywany z powrotem ze swoich pierwotnych bajtów
// Flagi pochodzenia mówią ci, co trzymasz i co może zostać przepisane
if Cache.FromRawBlobs then
begin
Writeln('stream id : ', IntToHex(Cache.StreamId, 4));
Writeln('legacy framing : ', Cache.RawFramingIsLegacy);
Writeln('own storage : ', Cache.RawHasStorageStream);
Writeln('model complete : ', Cache.RawModelIsComplete);
// Ponowna emisja jest bezstratna tylko, gdy każdy rekord ma tu swój model
if Cache.CanUpgradeFraming then
Writeln('safe to rewrite with the current emitters');
end;
Kiedy przepisanie cache jest bezstratne?
Tylko gdy trzy warunki zaszły razem, a CanUpgradeFraming to jedyna właściwość odpowiadająca na to pytanie. Cache musi wciąż być na surowym replayu, substream musi być w jednym z ramowań, które ta biblioteka kiedyś pisała błędnie, a reader musi mieć zbudowany kompletny typowany model każdego rekordu w nim. Cache napisany przez Excela nigdy się nie kwalifikuje, bo jego substream wozi rekordy, na które HotXLS nie ma modelu, a ponowna emisja z modelu by je zgubiła
Test kompletności jest surowszy, niż na pierwszy rzut oka wygląda. Rekord trzymany przez readera wyłącznie jako nieprzejrzyste bajty oznacza model niekompletny. Tak samo zadeklarowana liczba rekordów formuł, której emitter nie umie odtworzyć, bo ponowna emisja przepisałaby deklarację kilku rekordów formuł na deklarację żadnego, a wartość w pliku, której nie da się odtworzyć, jest równoważna rekordowi, którego nie da się odtworzyć
Świadomy konserwatyzm przebija też writeera. Indeksy są clampowane do legalnego zakresu, zamiast być kodowane jako out-of-band sentinela, bo specyfikacja definiuje indeks w sekwencję wartości wyróżnionych i nic więcej, a pusta komórka sama jest wartością tej sekwencji. Ciała rekordu cache przekraczającego sufit rekordu BIFF nie zapisuje się wcale, co wymagałoby tysięcy pól cache i tak jest nieosiągalne w limicie kolumn BIFF8; fallbackiem jest to, że Excel odświeża z zakresu źródłowego, co jest zdefiniowanym zachowaniem, a nie zepsutym plikiem
Daty niosą ostatnią zależność międzyrekordową. Konwersja numeru seryjnego na datę zależy od systemu dat skoroszytu, a emitter rekordów nie widzi skoroszytu, więc wybór daty bazowej jest przekazywany parametrem domyślnie ustawionym na system 1900 i dostarczanym przez ścieżkę zapisu poziomu skoroszytu. Pod systemem 1900 numer seryjny jest wartością wprost; system 1904 różni się o 1462 dni. Szerze potraktowanie numerów seryjnych dat jest w artykule numery seryjne dat, system 1904 i formaty liczb
Jeśli pracujesz na warstwie widoku, a nie cache, rekordy opisujące widoczną tabelę przestawną opisuje zestaw rekordów PivotTable BIFF8, a zachowanie po stronie obliczeń pola i elementy wyliczane oraz odświeżanie. Wszystkie trzy warstwy są częścią komponentu arkuszy HotXLS dla Delphi, co umożliwia załadowanie legacy'owego skoroszytu, zbadanie, co jego cache faktycznie zawiera, i rozstrzygnięcie, czy przepisanie jest bezpieczne, zanim to zrobisz