Åbn en gammel xls, gem den igen, og den add-in-formel, der kaldte ind i et registreret analysebibliotek, peger nu på en tom reference inde i arbejdsbogen selv. HotXLS sporer den stille korruption tilbage til én forkert antagelse: at en BIFF SupBook-record enten er self eller en ekstern fil. [MS-XLS] definerer syv slags, ikke to
Hvorfor mister en gemt arbejdsbog sine add-in-links?
Fordi klassificeringstesten var strukturel i stedet for typet. Den traditionelle genvej læser en SupBook-record ($01AE), kontrollerer, om den bærer self-markøren, og hvis ikke, behandler den streng, der følger, som en dokument-URL. Hver record, der er ingen af de to ting, falder igennem til en standardgren, og standardgrenen er næsten altid dette er arbejdsbogen selv. Et add-in-understøttende link, et same-sheet-link, en ubrugt plads og en afkortet record ender alle med samme forkerte etiket. Intet kaster en fejl, mens det sker: recorden parsed, formlen rekompileret, filen gemt uden en advarsel, og defekten dukker op tre uger senere, når nogen bemærker en kolonne af nuller, hvor en valutakonvertering plejede at være. [MS-XLS] §2.4.271 beskriver en record, der kan være en selvreference, en same-sheet-reference, en add-in-funktionsbeholder, en ekstern arbejdsbog med en virtuel sti og en arknavnetabel, et DDE- eller OLE-datalink eller en ubrugt pladsholder — og en syvende tilstand, der ikke er i specifikationen, men findes på rigtige diske, recorden, der ikke parser. Retsmidlet er ikke en bedre heuristik; det er at nægte at have en heuristik overhovedet
De syv slags, en SupBook-record kan bære
HotXLS erklærer supporting-link-taxonomien som en lukket enummeration i lxExternSheet.pas, og hver downstream-beslutning skifter på den. Ni enummerationsværdier dækker de syv kategorier, fordi DDE- og OLE-tilfældet behøver en provisorisk tilstand, før det kan løses:
type
TXLSSupportingLinkKind = (
slkUnknown, // fejlede at parse, eller der var resterende bytes
slkSelf, // denne arbejdsbog
slkSameSheet, // U+0000-markør
slkAddIn, // add-in-funktionsbeholder
slkExternalWorkbook, // virtuel sti + arknavnetabel
slkDde, // løst ud fra ExternName-flag
slkOle, // løst ud fra ExternName-flag
slkDdeOrOle, // én af de to, endnu ikke kendt hvilken
slkUnused); // pladsholder med ét mellemrum
TXLSFormulaReferenceClass = (
frcInternal,
frcExternalWorkbook,
frcExternalOther,
frcUnknownOrMalformed);
TXLSXtiInfo = record
XtiIndex : Integer; // nulbaseret, som gemt i ExternSheet.rgXTI
ExternID : Integer; // enbaseret, den interne konvention
SupBookIndex: Integer;
Sheet1Index : Integer;
Sheet2Index : Integer;
LinkKind : TXLSSupportingLinkKind;
end;
Afsendelsen er sentinel-drevet, ikke strengdrevet. En feltværdi på $0401 markerer self-recorden. Et arktal på ét parret med $3A01 markerer en add-in-beholder. Kun en værdi i intervallet 1 til $00FF betyder, at der følger en kodet virtuel sti, og først da afkoder HotXLS overhovedet en streng. Alt uden for de tre former forbliver slkUnknown, og en record, hvis arknavnetabel ikke forbruger recordkroppen præcist, degraderes tilbage til slkUnknown, selv når hovedet så plausibelt ud
Hvorfor afkodes same-sheet-markøren som en tom streng?
Fordi den generiske BIFF-strenglæser ødelægger den byte, som klassificeringen afhænger af. Same-sheet-supporting-linket er en streng på ét tegn, hvis eneste tegn er U+0000, og TXLSBlob.GetBiffString overrækker den som en tom WideString, uadskillelig fra en reelt tom sti — hvilket er præcis det input, en selvreference-heuristik svarer self til. HotXLS læser derfor den rå første kodepoint ud af recordkroppen i stedet for at stole på den afkodede værdi:
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) // komprimeret, én byte
else
FirstChar := Data.GetWord(StringOffset + 3); // bred, to byte
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;
Bemærk grenen komprimeret-eller-bred. Optionsbyten ligger på et fast offset fra strengheaderen, og den første kodepoint er én eller to byte afhængigt af bit 0, så at læse den som en byte ubetinget virker på de fleste filer og fejler på dem, der er skrevet af lokaliserede builds — den værst mulige fordeling for en fejl. Den ubrugte pladsholder fanges på samme måde, ved sin literale payload med ét mellemrum, og DDE- eller OLE-tilfældet ved U+0003-separatoren indlejret i den kodede sti
Hvorfor kan DDE og OLE ikke adskilles ved SupBook-tidspunktet?
Fordi SupBook-recorden ikke bærer de skelnenende bit. Den fortæller dig, at linket er én af de to; flagene fOle og fOleLink, der afgør hvilket, ligger i ExternName-recorden ($0023), som ankommer senere i strømmen. HotXLS noterer slkDdeOrOle ved parsetidspunktet og snævrer det ind i ParseExternalName, og hvis der aldrig ankommer en ExternName, forbliver slagsen provisorisk for altid — hvilket er korrekt, fordi filen reelt ikke siger det. Hver forbruger downstream behandler den provisoriske værdi som en rigtig værdi frem for en manglende, så ingen kalder behøver at opfinde et tiebreak. At gætte sandsynligvis DDE her ville købe en pænere enummeration og en klasse af forkerte svar, ingen kunne spore tilbage:
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-indekser er nulbaserede på disken og enbaserede internt
HotXLS udfører off-by-one-konverteringen præcis én gang, i det øjeblik et token kommer ind i det interne syntakstræ, og ingen andre steder. PtgNameX.ixti ([MS-XLS] §2.5.198.85) er et nulbaseret indeks ind i rgXTI-arrayet i ExternSheet-recorden ($0017, §2.4.106), mens bibliotekets interne ExternID-konvention er enbaseret med nul reserveret til ingen eksternt ark. BIFF8-læsestien gør FExternID := wValue + 1, når den afkoder et tNameX-token, og skrivestien emitterer StoreExternID - 1, hvilket efterlader det rå tokenview og semantikken på disken urørte. At tage fejl her er usædvanligt svært at fange: eksterne definerede navne løses til naboposten, og i en fil med ét enkelt XTI-indeks bliver indeks 0 til indeks 1, rammer ved siden af, og navnet degraderer stille. En regression, der kun øver rekompileret formeltekst, ser den aldrig, fordi rekompilering slet ikke rører diskindekset — samme fælde, der gør definerede navne på tværs af ark og arbejdsbøger værd at teste mod rigtige bytestrømme. Løsningen er afgrænset i begge ender: TlxExternSheetSheet.TryResolveXti returnerer False for et negativt indeks eller en manglende post, TXLSSupBook.TryGetKind returnerer False for et SupBook-indeks uden for arrayet, og ClassifyXti afbilder derefter slkSelf og slkSameSheet til frcInternal, slkExternalWorkbook til frcExternalWorkbook, og slkAddIn, slkDde, slkOle og slkDdeOrOle til frcExternalOther. Alt andet, enhver sti uden for interval inkluderet, lander på frcUnknownOrMalformed
Klassificering af en formel, før den fryses
TXLSCompiledFormula.ClassifyReferences scanner den bevarede BIFF-tokenstrøm direkte i stedet for at dekompilere formlen og lede efter firkantede parenteser. Jagt på parenteser i formeltekst er en tekstheuristik i en parsers frakke: den matcher strengliteraler, den matcher strukturerede referencer, og den overser eksterne definerede navne fuldstændigt, da de ikke bærer parenteser i dekompileret form. Tokenscanningen kigger kun på PtgNameX, PtgRef3d, PtgArea3d, PtgRefErr3d og PtgAreaErr3d og falder tilbage til en gennemgang af syntakstræet, når ingen BIFF-strøm overlever. Sammenlægningen er bevidst pessimistisk — den faste prioritet er frcUnknownOrMalformed, derefter frcExternalWorkbook, derefter frcExternalOther, derefter frcInternal — så et enkelt ulæsbart token forgifter hele formlen. For et eksternt defineret navn valideres navneindekset også: enbaseret, i intervallet og bakket op af en bevaret ExternName-record
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 er enbaseret
begin
Sheet := Wb.Sheets[i];
// fryser KUN formler klassificeret frcExternalWorkbook;
// interne, add-in-, DDE/OLE- og misdannede referencer forbliver formler
Sheet.ConvertFormulasToValues(True);
end;
Wb.SaveAs('quarterly-detached.xls');
finally
Wb.Free;
end;
end;
Parameteren OnlyExternal er stedet, hvor taxonomien betaler sig. At fryse en formel er irreversibelt, så operationen skal bevise, at en reference er en ekstern arbejdsbog, frem for blot at mistænke det. Add-in-kald overlever, DDE- og OLE-links overlever, og alt, hvad parseren ikke kunne forstå fuldt ud, overlever, fordi det sikre udfald af usikkerhed er at ændre ingenting. Samme disciplin styrer genbinding af formler kopieret mellem arbejdsbøger, hvor en fejlklassificeret reference genbindes til den forkerte arbejdsbog i stedet for at fejle med larm
Records, der ikke parser, skrives tilbage urørte
HotXLS bevarer den oprindelige SupBook-payload og emitterer den byte for byte igen, når recorden aldrig blev redigeret. En parsefejl sætter slkUnknown og rydder den afledte tilstand, men den fangede krop forbliver i FRawData, og gemmestien foretrækker den frem for enhver rekonstruktion, så længe elementet ikke er dirty og ikke er self-recorden. Alternativet — at normalisere en uparset record til en selvreference, så skriveren har noget velformet at emittere — forvandler en record, du ikke forstod, til en record, der er definitivt forkert. Det princip er samme kontrakt, som anvendes på VBA-projekter og deres eksterne referencer på tværs af en load-and-save-cyklus, og det er forskellen mellem et bibliotek, der round-tripper virkelighedens filer, og et, der round-tripper de filer, dets testpakke tilfældigt indeholder. En arbejdsbog, der har passeret gennem femten års Excel-versioner, en rapportgenerator og to migreringsværktøjer, vil indeholde records, ingen nu levende har designet. Skriv dem tilbage, som du fandt dem
Typet klassificering af SupBook- og XTI-records kom med HotXLS 2.361.2 til 2.361.4 sammen med afgrænset XTI-løsning og den sikrere ConvertFormulasToValues-sti beskrevet her. Hvis du vedligeholder Delphi- eller C++Builder-kode, der læser gamle xls-filer med add-in-kald, DDE- eller OLE-links eller eksterne definerede navne, håndterer HotXLS Delphi spreadsheet component hele taxonomien nativt, uden Excel-installation og uden OLE-automatisering på maskinen, der udfører arbejdet