Otevřete staré xls, uložte ho znovu, a vzorec doplňku, který volal do zaregistrované analytické knihovny, teď ukazuje na prázdnou referenci uvnitř samotného sešitu. HotXLS vystopuje to tiché poškození k jedné špatné domněnce: že záznam BIFF SupBook je buď self, nebo externí soubor. [MS-XLS] definuje sedm druhů, ne dva
Proč uložený sešit ztrácí odkazy doplňků?
Protože klasifikační test byl strukturální místo typovaný. Tradiční zkratka načte záznam SupBook ($01AE), ověří, zda nese značku self, a když ne, bere co následuje jako adresu dokumentu. Každý záznam, který ani to ani ono není, propadne do výchozí větve a výchozí větev skoro vždy říká „toto je samotný sešit“. Podpůrný odkaz doplňku, odkaz na stejný list, nepoužitý slot a zkrácený záznam si nakonec všechny obléknou tutéž špatnou etiketu. Nic nehodí výjimku, zatímco se to děje: záznam se rozparsoval, vzorec zkompiloval znovu, soubor se uložil bez varování a vada vyjde najevo o tři týdny později, když si někdo všimne sloupce nul tam, kde bývala převod měny. [MS-XLS] §2.4.271 popisuje záznam, který může být samoodkazem, odkazem na stejný list, kontejnerem funkcí doplňku, externím sešitem s virtuální cestou a tabulkou názvů listů, datovým odkazem DDE nebo OLE, nebo nepoužitou zástupkou — a sedmý stav, který specifikace nezná, ale na reálných discích existuje: záznam, který se neparsuje. Oprava není lepší heuristika; je to odmítnutí heuristiky úplně
Sedm druhů, které záznam SupBook může nést
HotXLS deklaruje taxonomii podpůrných odkazů jako uzavřený výčet v lxExternSheet.pas a každé následné rozhodnutí se na něj přepíná. Devět hodnot výčtu pokrývá sedm kategorií, protože případ DDE a OLE potřebuje přechodný stav, než se dá rozřešit:
type
TXLSSupportingLinkKind = (
slkUnknown, // nepodařilo se rozparsovat, nebo zbyly bajty na konci
slkSelf, // tento sešit
slkSameSheet, // značka U+0000
slkAddIn, // kontejner funkcí doplňku
slkExternalWorkbook, // virtuální cesta + tabulka názvů listů
slkDde, // rozřešeno z příznaků ExternName
slkOle, // rozřešeno z příznaků ExternName
slkDdeOrOle, // jeden z obou, ještě není známo který
slkUnused); // zástupka z jediné mezery
TXLSFormulaReferenceClass = (
frcInternal,
frcExternalWorkbook,
frcExternalOther,
frcUnknownOrMalformed);
TXLSXtiInfo = record
XtiIndex : Integer; // od nuly, jak je uloženo v ExternSheet.rgXTI
ExternID : Integer; // od jedničky, interní konvence
SupBookIndex: Integer;
Sheet1Index : Integer;
Sheet2Index : Integer;
LinkKind : TXLSSupportingLinkKind;
end;
Rozesílání řídí strážná hodnota, ne řetězce. Hodnota pole $0401 značí záznam self. Počet listů jedna spárovaný s $3A01 značí kontejner doplňku. Jen hodnota v rozsahu 1 až $00FF znamená, že následuje zakódovaná virtuální cesta, a jen tehdy HotXLS vůbec dekóduje řetězec. Cokoliv mimo ty tři tvary zůstává slkUnknown a záznam, jehož tabulka názvů listů nespotřebuje tělo záznamu přesně, se srazí zpět na slkUnknown, i když hlava vypadala věrohodně
Proč se značka stejného listu dekóduje jako prázdný řetězec?
Protože všeobecný čtenář BIFF řetězců ničí bajt, na kterém klasifikace stojí. Podpůrný odkaz na stejný list je jednoznakový řetězec, jehož jediný znak je U+0000, a TXLSBlob.GetBiffString ho vrátí jako prázdný WideString, nerozlišitelný od skutečně prázdné cesty — což je přesně ten vstup, na který heuristika samoodkazu odpoví „self“. HotXLS proto čte surový první kódový bod přímo z těla záznamu, místo aby věřil dekódované hodnotě:
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); // široký, 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šimněte si větve komprimovaný proti širokému. Bajt voleb sedí na pevném posunu od hlavičky řetězce a první kódový bod je jeden bajt nebo dva podle bitu 0, takže bezpodmínečné čtení jako bajtu funguje na většině souborů a selhává na těch z lokalizovaných sestavení — co nejhůře možné rozložení pro chybu. Nepoužitá zástupka se odchytí stejným způsobem, podle svého doslovného obsahu z jediné mezery, a případ DDE nebo OLE podle oddělovače U+0003 zaneseného v zakódované cestě
Proč nelze DDE a OLE oddělit už při čtení SupBook?
Protože záznam SupBook nenese rozhodující bity. Řekne vám, že odkaz je jeden z obou; příznaky fOle a fOleLink, které rozhodují který, žijí v záznamu ExternName ($0023) dorazivším ve streamu později. HotXLS zaznamená slkDdeOrOle při parsování a zúží ho v ParseExternalName, a když žádné ExternName nikdy nedorazí, druh zůstane přechodný navždy — což je správně, protože soubor to doopravdy neříká. Každý konzument dále po proudu bere tu přechodnou hodnotu jako skutečnou hodnotu, ne jako chybějící, takže žádný volající si nemusí vymýšlet rozsuďák. Hádání „asi DDE“ tady by koupilo úhlednější výčet a třídu špatných odpovědí, které by nikdo nevystopoval:
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;
Indexy XTI jsou na disku od nuly a uvnitř od jedničky
HotXLS provádí konverzi o jedničku přesně jednou, v bodě, kdy token vstupuje do interního syntaktického stromu, a nikde jinde. PtgNameX.ixti ([MS-XLS] §2.5.198.85) je index od nuly do pole rgXTI záznamu ExternSheet ($0017, §2.4.106), zatímco interní konvence ExternID knihovny je od jedničky s nulou rezervovanou pro „žádný externí list“. Čtenářská cesta BIFF8 udělá FExternID := wValue + 1, když dekóduje token tNameX, a zapisovací cesta vydá StoreExternID - 1, takže surový pohled na tokeny i sémantika na disku zůstanou nedotčené. Pokazit to se neobyčejně špatně chytá: externí definované názvy se rozřešou na vedlejší položku a v souboru s jedinou položkou XTI se index 0 stane indexem 1, mine se a název tiše degraduje. Regrese, která cvičí jen rekompilovaný text vzorců, ji nikdy neuzří, protože rekompilace diskového indexu vůbec nedotkne — stejná past, kvůli které stojí definované názvy přesahující listy a sešity za testování na skutečných bajtových streamech. Rozřešení je ohraničené na obou koncích: TlxExternSheetSheet.TryResolveXti vrátí False pro záporný index nebo chybějící položku, TXLSSupBook.TryGetKind vrátí False pro index SupBook mimo pole a ClassifyXti pak mapuje slkSelf a slkSameSheet na frcInternal, slkExternalWorkbook na frcExternalWorkbook a slkAddIn, slkDde, slkOle a slkDdeOrOle na frcExternalOther. Všechno ostatní, včetně každé cesty mimo rozsah, dopadne na frcUnknownOrMalformed
Klasifikace vzorce před zmrznutím
TXLSCompiledFormula.ClassifyReferences skenuje zachovaný BIFF tokenový stream přímo místo dekompilace vzorce a hledání hranatých závorek. Lov závorek v textu vzorce je textová heuristika v plášti parseru: trefí řetězcové literály, trefí strukturované odkazy a externí definované názvy minete celé, protože ty v dekompilované podobě žádné závorky nenosí. Tokenový sken hledí jen na PtgNameX, PtgRef3d, PtgArea3d, PtgRefErr3d a PtgAreaErr3d a sestupuje k procházení syntaktického stromu, když BIFF stream nepřežije. Slučování je záměrně pesimistické — pevná priorita je frcUnknownOrMalformed, pak frcExternalWorkbook, pak frcExternalOther, pak frcInternal — takže jediný nečitelný token otráví celý vzorec. U externího definovaného názvu se ověřuje i index názvu: od jedničky, v rozsahu a podložený uchovaným záznamem 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 jedničky
begin
Sheet := Wb.Sheets[i];
// zmrznou JEN vzorce klasifikované jako frcExternalWorkbook;
// interní, doplňkové, DDE/OLE a znetvořené odkazy zůstanou vzorci
Sheet.ConvertFormulasToValues(True);
end;
Wb.SaveAs('quarterly-detached.xls');
finally
Wb.Free;
end;
end;
Parametr OnlyExternal je místo, kde se taxonomie vyplatí. Zmrznutí vzorce je nevratné, takže operace musí dokázat, že odkaz je externí sešit, ne ho jen podezírat. Volání doplňků přežijí, odkazy DDE a OLE přežijí a všechno, čemu parser plně nerozuměl, přežije, protože bezpečný výsledek nejistoty je nic neměnit. Tatáž disciplína řídí přepojení vzorců kopírovaných mezi sešity, kde špatně klasifikovaný odkaz se přepojí na špatný sešit místo hlasitého selhání
Záznamy, které se neparsují, se zapisují zpět nedotčené
HotXLS si drží původní náklad záznamu SupBook a vydává ho bajt za bajt, když záznam nikdy nebyl editován. Selhání parsování nastaví slkUnknown a vyčistí odvozený stav, ale zachycené tělo zůstává v FRawData a zapisovací cesta mu dává přednost před jakýmkoli rekonstruováním, dokud položka není špinavá a nejde o záznam self. Alternativa — normalizovat neparseovaný záznam na samoodkaz, aby měl zapisovač něco dobře utvářeného k vydání — mění záznam, kterému jste nerozuměli, na záznam, který je jednoznačně špatně. Ten princip je tatáž smlouva aplikovaná na VBA projekty a jejich externí odkazy přes cyklus načtení a uložení, a je to rozdíl mezi knihovnou, která zpracovává tam a zpět reálné soubory, a knihovnou, která zpracovává soubory, které náhodou obsahuje její testovací sada. Sešit, který prošel patnácti lety verzí Excelu, generátorem sestav a dvěma migračními nástroji, bude obsahovat záznamy, které nikdo žijící nenavrhl. Zapisujte je zpět, jak jste je našli
Typovaná klasifikace záznamů SupBook a XTI dorazila v HotXLS 2.361.2 až 2.361.4 spolu s ohraničeným rozřešením XTI a bezpečnější cestou ConvertFormulasToValues popsanou tady. Pokud udržujete kód Delphi nebo C++Builder, který čte staré soubory xls nesoucí volání doplňků, odkazy DDE nebo OLE nebo externí definované názvy, HotXLS Delphi tabulková komponenta zvládne celou taxonomii nativně, bez instalace Excelu a bez OLE automatizace na stroji, který dělá práci