Technisch artikel

SupBook en XTI: externe koppelingen classificeren in Delphi

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

De sentinel-gedreven ladder waarmee HotXLS een BIFF SupBook-record in zeven soorten classificeert, met alleen voor waarden in het encoded-path-bereik een string decodeert en terugvalt op een onbekende soort in plaats van op een default branch
Elke soort wordt bereikt via een sentinel in plaats van via een stringtest, en een record dat geen van de vormen matcht blijft onbekend in plaats van in een default branch te vallen die deze werkmap betekent

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 HotXLS de rauwe eerste codepunt uit een BIFF SupBook-recordbody leest in plaats van de gedecodeerde string, omdat de algemene stringlezer de same-sheet U+0000-marker in een lege waarde vouwt
De same-sheet-marker is een string van één teken waarvan het teken U+0000 is, dus de algemene stringlezer vouwt hem in een lege waarde en alleen de rauwe codepunt op de optiebyte-offset houdt hem vast

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

HotXLS die de zero-based XTI-index van een BIFF PtgNameX-token op één punt omzet naar zijn one-based interne ExternID, met begrensde resolutie aan beide uiteinden en de classificatiekaart die hem verbruikt
De off-by-one tussen de zero-based schijfindex en de one-based interne ExternID wordt één keer toegepast, wanneer een token de syntaxisboom binnengaat, en elke onoplosbare index belandt op de malformed-klasse

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