Open een oude xls, sla hem opnieuw op, en de add-in-formule die een geregistreerde analysebibliotheek aanriep wijst nu op een lege referentie binnen de werkmap zelf. HotXLS spoor die stille corruptie terug naar één foute aanname: dat een BIFF SupBook-record of self is of een extern bestand. [MS-XLS] definieert zeven soorten, niet twee
Waarom verliest een opgeslagen werkmap zijn add-in-koppelingen?
Omdat de classificatietest structureel was in plaats van getypeerd. De traditionele shortcut leest een SupBook-record ($01AE), controleert of het de self-marker draagt, en zo niet, behandelt welke string er ook volgt als een document-URL. Elk record dat geen van die twee dingen is valt door in een default branch, en de default branch is bijna altijd dit is de werkmap zelf. Een add-in supporting link, een same-sheet-koppeling, een ongebruikte slot en een afgebroken record belanden allemaal in hetzelfde verkeerde etiket. Niets gooit een exceptie terwijl dit gebeurt: het record parseerde, de formule hercompileerde, het bestand werd opgeslagen zonder waarschuwing, en het defect komt drie weken later boven water wanneer iemand een kolom nullen opmerkt waar vroeger een valutaconversie stond. [MS-XLS] §2.4.271 beschrijft een record dat een zelfverwijzing, een same-sheet-verwijzing, een container voor add-in-functies, een externe werkmap met een virtueel pad en een sheet-name-tabel, een DDE- of OLE-datalink, of een ongebruikte placeholder kan zijn — plus een zevende toestand die niet in de specificatie staat maar op echte schijven bestaat: het record dat niet parseert. De oplossing is geen betere heuristiek; het is weigeren überhaupt een heuristiek te hebben
De zeven soorten die een SupBook-record kan dragen
HotXLS declareert de supporting-link-taxonomie als een gesloten enumeratie in lxExternSheet.pas, en elke stroomafwaartse beslissing schakelt erop. Negen enumeratiewaarden dekken de zeven categorieën, omdat het DDE- en OLE-geval een voorlopige toestand nodig heeft voordat het kan worden opgelost:
type
TXLSSupportingLinkKind = (
slkUnknown, // parsen mislukt, of er bleven achterliggende bytes over
slkSelf, // deze werkmap
slkSameSheet, // U+0000-marker
slkAddIn, // container voor add-in-functies
slkExternalWorkbook, // virtueel pad + sheet-name-tabel
slkDde, // opgelost uit ExternName-vlaggen
slkOle, // opgelost uit ExternName-vlaggen
slkDdeOrOle, // één van de twee, nog onbekend welke
slkUnused); // placeholder van één spatie
TXLSFormulaReferenceClass = (
frcInternal,
frcExternalWorkbook,
frcExternalOther,
frcUnknownOrMalformed);
TXLSXtiInfo = record
XtiIndex : Integer; // zero-based, zoals opgeslagen in ExternSheet.rgXTI
ExternID : Integer; // one-based, de interne conventie
SupBookIndex: Integer;
Sheet1Index : Integer;
Sheet2Index : Integer;
LinkKind : TXLSSupportingLinkKind;
end;
De dispatch is sentinel-gedreven, niet string-gedreven. Een veldwaarde van $0401 markeert het self-record. Een sheet-aantal van één gekoppeld aan $3A01 markeert een add-in-container. Alleen een waarde in het bereik 1 tot $00FF betekent dat een gecodeerd virtueel pad volgt, en pas dan decodeert HotXLS überhaupt een string. Alles buiten die drie vormen blijft slkUnknown, en een record waarvan de sheet-name-tabel de recordbody niet exact opgebruikt wordt teruggeschaald naar slkUnknown, zelfs als de kop plausibel oogde
Waarom decodeert de same-sheet-marker als een lege string?
Omdat de algemene BIFF-stringlezer de byte vernielt waar de classificatie van afhangt. De same-sheet supporting link is een string van één teken waarvan dat ene teken U+0000 is, en TXLSBlob.GetBiffString geeft dat terug als een lege WideString, niet te onderscheiden van een werkelijk leeg pad — precies de input waarop een zelfverwijzingsheuristiek self antwoordt. HotXLS leest daarom de rauwe eerste codepunt uit de recordbody in plaats van op de gedecodeerde waarde te vertrouwen:
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) // gecomprimeerd, één byte
else
FirstChar := Data.GetWord(StringOffset + 3); // wide, twee bytes
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;
Let op de gecomprimeerd-versus-wide-branch. De optiebyte zit op een vaste offset vanaf de stringheader en de eerste codepunt is één of twee bytes afhankelijk van bit 0, dus hem onvoorwaardelijk als byte lezen werkt op de meeste bestanden en faalt op die van gelokaliseerde builds — de slechtst mogelijke verdeling voor een bug. De ongebruikte placeholder wordt op dezelfde manier gevangen, aan zijn letterlijke payload van één spatie, en het DDE- of OLE-geval aan de U+0003-separator die in het gecodeerde pad is ingebed
Waarom kunnen DDE en OLE niet bij SupBook-tijd worden gescheiden?
Omdat het SupBook-record de onderscheidende bits niet draagt. Het vertelt u dat de koppeling één van de twee is; de fOle- en fOleLink-vlaggen die bepalen welke wonen in het ExternName-record ($0023) dat later in de stream arriveert. HotXLS noteert slkDdeOrOle op parsetijd en vernauwt het in ParseExternalName, en als er nooit een ExternName komt blijft de soort voor altijd voorlopig — wat correct is, want het bestand zegt het echt niet. Elke consument stroomafwaarts behandelt die voorlopige waarde als een echte waarde in plaats van een ontbrekende, dus geen enkele aanroeper hoeft een tiebreak te verzinnen. Hier waarschijnlijk DDE gokken zou een nettere enumeratie opleveren en een klasse foute antwoorden die niemand kon terugtraceren:
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-indexen zijn zero-based op schijf en one-based vanbinnen
HotXLS voert de off-by-one-conversie exact één keer uit, op het moment dat een token de interne syntaxisboom binnengaat, en nergens anders. PtgNameX.ixti ([MS-XLS] §2.5.198.85) is een zero-based index in de rgXTI-array van het ExternSheet-record ($0017, §2.4.106), terwijl de interne ExternID-conventie van de bibliotheek one-based is met nul gereserveerd voor geen externe sheet. Het BIFF8-leespad doet FExternID := wValue + 1 wanneer het een tNameX-token decodeert en het schrijfpad emitteert StoreExternID - 1, waarbij de rauwe tokenweergave en de on-disk-semantiek onaangeroerd blijven. Dit fout doen is ongewoon moeilijk te vangen: externe gedefinieerde namen resolven naar de buureentry, en in een bestand met één XTI-entry wordt index 0 index 1, mist, en degradeert de naam stilletjes. Een regressie die alleen hercompileerde formulieretekst oefent ziet het nooit, want hercompilatie raakt de schijfindex helemaal niet — dezelfde valkuil die gedefinieerde namen over sheets en werkmappen het testen tegen echte bytestromen waard maakt. Resolutie is aan beide uiteinden begrensd: TlxExternSheetSheet.TryResolveXti geeft False bij een negatieve index of een ontbrekende entry, TXLSSupBook.TryGetKind geeft False bij een SupBook-index buiten de array, en ClassifyXti mapt daarna slkSelf en slkSameSheet naar frcInternal, slkExternalWorkbook naar frcExternalWorkbook, en slkAddIn, slkDde, slkOle en slkDdeOrOle naar frcExternalOther. Alles verder, elke out-of-range-route inbegrepen, belandt op frcUnknownOrMalformed
Een formule classificeren voordat u haar bevriest
TXLSCompiledFormula.ClassifyReferences scant de bewaarde BIFF-tokenstream rechtstreeks in plaats van de formule te decompileren en naar vierkante haken te zoeken. Haakjes jagen in formulieretekst is een tekstheuristiek in een parsejas: hij matcht stringliterals, hij matcht structured references, en hij mist externe gedefinieerde namen volledig, want die dragen geen haken in gedecompileerde vorm. De tokenscan kijkt alleen naar PtgNameX, PtgRef3d, PtgArea3d, PtgRefErr3d en PtgAreaErr3d, met terugval op een syntaxisboom-wandeling wanneer er geen BIFF-stream overleeft. Samenvoegen is bewust pessimistisch — de vaste prioriteit is frcUnknownOrMalformed, dan frcExternalWorkbook, dan frcExternalOther, dan frcInternal — dus één onleesbaar token vergiftigt de hele formule. Voor een externe gedefinieerde naam wordt ook de naamindex gevalideerd: one-based, in bereik, en gedekt door een behouden 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 is one-based
begin
Sheet := Wb.Sheets[i];
// bevriest ALLEEN formules geclassificeerd als frcExternalWorkbook;
// interne, add-in-, DDE/OLE- en misvormde referenties blijven formules
Sheet.ConvertFormulasToValues(True);
end;
Wb.SaveAs('quarterly-detached.xls');
finally
Wb.Free;
end;
end;
De parameter OnlyExternal is waar de taxonomie zichzelf terugverdient. Een formule bevriezen is onomkeerbaar, dus de operatie moet bewijzen dat een referentie een externe werkmap is in plaats van haar slechts te vermoeden. Add-in-aanroepen overleven, DDE- en OLE-koppelingen overleven, en alles wat de parser niet volledig kon begrijpen overleeft, want de veilige uitkomst van onzekerheid is niets veranderen. Dezelfde discipline regeert over formules die tussen werkmappen zijn gekopieerd opnieuw binden, waar een verkeerd geclassificeerde referentie aan de verkeerde map wordt gebonden in plaats van luid te falen
Records die niet parsen worden onaangeroerd teruggeschreven
HotXLS houdt de originele SupBook-payload vast en emitteert hem byte voor byte opnieuw wanneer het record nooit is bewerkt. Een parsefout zet slkUnknown en wist de afgeleide toestand, maar de vastgelegde body blijft in FRawData en het opslagpad verkiest haar boven elke reconstructie zolang het item niet dirty is en niet het self-record is. Het alternatief — een ongeparseerd record normaliseren naar een zelfverwijzing zodat de writer iets goedgevormds heeft om te emitteeren — maakt van een record dat u niet begreep een record dat definitief fout is. Dat principe is hetzelfde contract als toegepast op VBA-projecten en hun externe referenties over een load-and-save-cyclus, en het is het verschil tussen een bibliotheek die echte bestanden round-tript en één die de bestanden round-tript die zijn testsuite toevallig bevat. Een werkmap die door vijftien jaar Excel-versies, een rapportgenerator en twee migratietools is gegaan bevat records die niemand die nu leeft heeft ontworpen. Schrijf ze terug zoals u ze aantrof
Getypeerde classificatie van SupBook- en XTI-records verscheen in HotXLS 2.361.2 tot 2.361.4, samen met begrensde XTI-resolutie en het veiligere ConvertFormulasToValues-pad dat hier wordt beschreven. Onderhoudt u Delphi- of C++Builder-code die legacy xls-bestanden leest met add-in-aanroepen, DDE- of OLE-koppelingen, of externe gedefinieerde namen, dan behandelt de HotXLS Delphi spreadsheet component de hele taxonomie native, zonder Excel-installatie en zonder OLE-automation op de machine die het werk doet