Otvorte starý xls, uložte ho znova a add-in vzorec, ktorý volal do zaregistrovanej analytickej knižnice, teraz ukazuje na prázdnu referenciu vnútri samotného zošita. HotXLS vystopuje toto tiché poškodenie k jednému zlému predpokladu: že záznam BIFF SupBook je buď self, alebo externý súbor. [MS-XLS] definuje sedem druhov, nie dva
Prečo uložený zošit stráca svoje add-in odkazy?
Pretože klasifikačný test bol štrukturálny namiesto typovaného. Tradičná skratka číta záznam SupBook ($01AE), kontroluje, či nesie self marker, a ak nie, považuje akýkoľvek reťazec, ktorý nasleduje, za URL dokumentu. Každý záznam, ktorý nie je ani jedna z tých dvoch vecí, spadne do predvolenej vetvy a predvolená vetva je takmer vždy „toto je samotný zošit". Podporujúci odkaz add-inu, odkaz same-sheet, nepoužitý slot a skrátený záznam všetky skončia s rovnakou zlou nálepkou. Nič nespadne, kým sa to deje: záznam sa rozparsuje, vzorec sa znova zkompiluje, súbor sa uloží bez varovania a defekt sa vynorí o tri týždne, keď niekto zaznamená stĺpec nulov tam, kde bývala menová konverzia. [MS-XLS] §2.4.271 popisuje záznam, ktorý môže byť self-referenciou, referenciou same-sheet, kontajnerom add-in funkcií, externým zošitom s virtuálnou cestou a tabuľkou názvov listov, DDE alebo OLE dátovým odkazom alebo nepoužitým zástupným symbolom — a siedmy stav, ktorý nie je v špecifikácii, ale existuje na reálnych diskoch: záznam, ktorý sa nedá rozparsovať. Oprava nie je lepšia heuristika; je to odmietnutie mať vôbec nejakú heuristiku
Sedem druhov, ktoré môže záznam SupBook niesť
HotXLS deklaruje taxonómiu podporujúcich odkazov ako uzavretú enumeráciu v lxExternSheet.pas a každé downstream rozhodnutie sa prepína na nej. Deväť hodnôt enumerácie pokrýva sedem kategórií, pretože prípad DDE a OLE potrebuje prechodný stav, kým sa dá rozlíšiť:
type
TXLSSupportingLinkKind = (
slkUnknown, // nepodarilo sa rozparsovať, alebo ostali zvyškové bajty
slkSelf, // tento zošit
slkSameSheet, // marker U+0000
slkAddIn, // kontajner add-in funkcií
slkExternalWorkbook, // virtuálna cesta + tabuľka názvov listov
slkDde, // rozlíšené z vlajok ExternName
slkOle, // rozlíšené z vlajok ExternName
slkDdeOrOle, // jedno z oboch, ešte nie je známe ktoré
slkUnused); // zástupný symbol jednej medzery
TXLSFormulaReferenceClass = (
frcInternal,
frcExternalWorkbook,
frcExternalOther,
frcUnknownOrMalformed);
TXLSXtiInfo = record
XtiIndex : Integer; // od nuly, ako je uložené v ExternSheet.rgXTI
ExternID : Integer; // od jednotky, interná konvencia
SupBookIndex: Integer;
Sheet1Index : Integer;
Sheet2Index : Integer;
LinkKind : TXLSSupportingLinkKind;
end;
Dispatch je riadený sentinelmi, nie reťazcami. Hodnota poľa $0401 označuje self záznam. Počet listov jedna spolu s $3A01 označuje kontajner add-inu. Len hodnota v rozsahu 1 až $00FF znamená, že nasleduje kódovaná virtuálna cesta, a až potom HotXLS vôbec dekóduje reťazec. Čokoľvek mimo tých troch tvarov ostáva slkUnknown a záznam, ktorej tabuľka názvov listov nespotrebuje telo záznamu presne, sa degraduje späť na slkUnknown, aj keď hlava vyzerala dôveryhodne
Prečo sa marker same-sheet dekóduje ako prázdny reťazec?
Pretože všeobecný BIFF čítač reťazcov zničí bajt, na ktorom klasifikácia závisí. Podporujúci odkaz same-sheet je jednoseznakový reťazec, ktorého jediný znak je U+0000, a TXLSBlob.GetBiffString ho vráti ako prázdny WideString, neodlíšiteľný od skutočne prázdnej cesty — čo je presne ten vstup, na ktorý heuristika self-referencie odpovedá „self". HotXLS preto číta surový prvý kódový bod priamo z tela záznamu namiesto dôvery v dekódovanú hodnotu:
StringOffset := offset;
FDocUrl := Data.GetBiffString(offset, False, True);
FirstChar := $FFFF;
if val = 1 then
begin
StringOptions := Data.GetByte(StringOffset + 2);
if (StringOptions and $01) = 0 then
FirstChar := Data.GetByte(StringOffset + 3) // komprimované, jeden bajt
else
FirstChar := Data.GetWord(StringOffset + 3); // wide, dva bajty
end;
if FirstChar = 0 then
FKind := slkSameSheet
else if (Length(FDocUrl) = 1) and (FDocUrl[1] = WideChar(#32)) then
FKind := slkUnused
else if Pos(WideChar(#3), FDocUrl) > 0 then
FKind := slkDdeOrOle
else if FDocUrl <> '' then
FKind := slkExternalWorkbook;
Všimnite si vetvu komprimované-versus-wide. Bajt volieb sedí na fixnom ofsete od hlavičky reťazca a prvý kódový bod je jeden bajt alebo dva podľa bitu 0, takže jeho čítanie ako bajtu bezpodmienečne funguje na väčšine súborov a zlyháva na tých zapísaných lokalizovanými zostavami — najhoršie možné rozdelenie pre chybu. Nepoužitý zástupný symbol sa chytí rovnakým spôsobom, podľa jeho doslovného payloadu jednej medzery, a prípad DDE alebo OLE podľa separátora U+0003 vloženého do kódovanej cesty
Prečo sa DDE a OLE nedajú oddeliť v čase SupBook?
Pretože záznam SupBook nesie odlišujúce bity nie. Hovorí vám, že odkaz je jedno z oboch; vlajky fOle a fOleLink, ktoré rozhodujú ktoré, žijú v zázname ExternName ($0023) prichádzajúcom neskôr v streame. HotXLS zaznamená slkDdeOrOle v čase parsovania a zúži to v ParseExternalName a ak žiadny ExternName nikdy nepríde, druh ostáva prechodný navždy — čo je správne, pretože súbor to skutočne nepovie. Každý downstream konzument považuje tú prechodnú hodnotu za skutočnú hodnotu, nie za chýbajúcu, takže žiadny volajúci si nemusí vymýšľať rozhodovacie pravidlo. Hádzať „pravdepodobne DDE" tu by kúpilo upratanejšiu enumeráciu a triedu zlých odpovedí, ktoré nikto nedokáže vystopovať späť:
if FKind = slkDdeOrOle then
begin
if Data.DataLength < 2 then
Exit;
Flags := Data.GetWord(0);
if (Flags and $0010) <> 0 then
FKind := slkOle
else if (Flags and $0008) <> 0 then
FKind := slkDde;
end;
XTI indexy sú na disku od nuly a vnútri od jednotky
HotXLS vykoná konverziu off-by-one presne raz, v mieste, kde token vstupuje do interného syntaktického stromu, a nikde inde. PtgNameX.ixti ([MS-XLS] §2.5.198.85) je index od nuly do poľa rgXTI záznamu ExternSheet ($0017, §2.4.106), zatiaľ čo interná konvencia ExternID knižnice je od jednotky s nulou rezervovanou pre „žiadny externý list". Čítacia cesta BIFF8 robí FExternID := wValue + 1, keď dekóduje token tNameX a zápisová cesta emituje StoreExternID - 1, pričom ponecháva surový pohľad tokenu a diskové sémantiky nedotknuté. Pokaziť to je neobyčajne ťažké chytiť: externé definované názvy sa rozlíšia na susednú položku a v súbore s jedinou XTI položkou sa index 0 stane indexom 1, minie a názov sa mlčky degraduje. Regresia, ktorá cvičí iba znovu zkompilovaný text vzorcov, ho nikdy nevidí, pretože rekompilácia sa vôbec nedotýka diskového indexu — tá istá pasca, ktorá robí definované názvy siahajúce cez listy a zošity hodnými testovania proti reálnym bajtovým streamom. Rozlíšenie je ohraničené na oboch koncoch: TlxExternSheetSheet.TryResolveXti vracia False pre negatívny index alebo chýbajúcu položku, TXLSSupBook.TryGetKind vracia False pre SupBook index mimo poľa a ClassifyXti potom mapuje slkSelf a slkSameSheet na frcInternal, slkExternalWorkbook na frcExternalWorkbook a slkAddIn, slkDde, slkOle a slkDdeOrOle na frcExternalOther. Všetko ostatné, každá cesta mimo rozsahu vrátane, pristane na frcUnknownOrMalformed
Klasifikácia vzorca pred zamrznutím
TXLSCompiledFormula.ClassifyReferences skenuje zachovaný BIFF token stream priamo namiesto dekompilácie vzorca a hľadania hranatých zátvoriek. Lovenie zátvoriek v texte vzorca je textová heuristika v kabáte parsera: zhoduje reťazcové literály, zhoduje štruktúrované referencie a externé definované názvy úplne minie, keďže tie nesú žiadne zátvorky v dekompilovanej forme. Token sken sa pozerá len na PtgNameX, PtgRef3d, PtgArea3d, PtgRefErr3d a PtgAreaErr3d s pádom späť na prechádzku syntaktického stromu, keď žiadny BIFF stream neprežije. Zlučovanie je zámerne pesimistické — pevná priorita je frcUnknownOrMalformed, potom frcExternalWorkbook, potom frcExternalOther, potom frcInternal — takže jediný nečitateľný token otrávi celý vzorec. Pre externý definovaný názov sa validuje aj index názvu: od jednotky, v rozsahu a podložený zachovaným záznamom ExternName
var
Wb : TXLSWorkbook;
Sheet: TXLSWorksheet;
i : Integer;
begin
Wb := TXLSWorkbook.Create;
try
Wb.Open('quarterly.xls');
for i := 1 to Wb.Sheets.Count do // Sheets je od jednotky
begin
Sheet := Wb.Sheets[i];
// zamrazuje IBA vzorce klasifikované ako frcExternalWorkbook;
// interné, add-in, DDE/OLE a malformed referencie ostávajú vzorcu
Sheet.ConvertFormulasToValues(True);
end;
Wb.SaveAs('quarterly-detached.xls');
finally
Wb.Free;
end;
end;
Parameter OnlyExternal je miesto, kde sa taxonómia zaplatí sama. Zamrznutie vzorca je nevratné, takže operácia musí dokázať, že referencia je externý zošit, nielen že je podozrivá. Add-in volania prežijú, DDE a OLE odkazy prežijú a čokoľvek, čo parser nemohol úplne pochopiť, prežije, pretože bezpečný výsledok neistoty je nezmeniť nič. Rovnaká disciplína riadi rebinding vzorcov kopírovaných medzi zošitmi, kde nesprávne klasifikovaná referencia rebinding na nesprávny zošit namiesto hlasného zlyhania
Záznamy, ktoré sa nedajú rozparsovať, sa zapisujú späť nedotknuté
HotXLS drží pôvodný payload SupBook a emituje ho znova bajt po bajte, keď záznam nikdy nebol upravený. Zlyhanie parsovania nastaví slkUnknown a vyčistí odvodený stav, ale zachytené telo ostáva v FRawData a cesta ukladania ho uprednostní pred akoukoľvek rekonštrukciou, pokiaľ položka nie je dirty a nie je self záznamom. Alternatíva — normalizácia nerozparsovaného záznamu na self-referenciu, aby mal zápisovač dobre tvarovanú vec na emitovanie — zmení záznam, ktorému ste nerozumeli, na záznam, ktorý je definitívne zlý. Ten princíp je rovnaký kontrakt aplikovaný na VBA projekty a ich externé referencie naprieč cyklom načítanie-a-uloženie a je to rozdiel medzi knižnicou, ktorá round-tripuje reálne súbory sveta, a tou, ktorá round-tripuje súbory, ktoré jej testovacia sada náhodou obsahuje. Zošit, ktorý prešiel pätnástimi rokmi verzií Excelu, generátorom reportov a dvoma migračnými nástrojmi, bude obsahovať záznamy, ktoré nikto súčasne žijúci nenavrhol. Zapíšte ich späť tak, ako ste ich našli
Typovaná klasifikácia záznamov SupBook a XTI bola dodaná v HotXLS 2.361.2 až 2.361.4 spolu s ohraničeným rozlíšením XTI a bezpečnejšou cestou ConvertFormulasToValues popísanou tu. Ak udržiavate Delphi alebo C++Builder kód, ktorý číta legacy xls súbory nesúce add-in volania, DDE alebo OLE odkazy alebo externé definované názvy, HotXLS Delphi spreadsheet component zvláda celú taxonómiu natívne, bez inštalácie Excelu a bez OLE automatizácie na stroji, ktorý robí prácu