Teknisk artikel

BIFF SupBook- og XTI-linkklassificering i Delphi

Å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

Den sentinel-drevne ladder, HotXLS bruger til at klassificere en BIFF SupBook-record i syv slags, og som kun afkoder en streng for værdier i det kodede sti-interval og falder tilbage til en ukendt slags i stedet for til en standardgren
Hver slags nås via en sentinel frem for via en strengtest, og en record, der matcher ingen af formerne, forbliver ukendt i stedet for at falde ind i en standardgren, der betyder denne arbejdsbog

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 HotXLS læser den rå første kodepoint ud af en BIFF SupBook-recordkrop i stedet for den afkodede streng, fordi den generiske strenglæser forvandler same-sheet U+0000-markøren til en tom værdi
Same-sheet-markøren er en streng på ét tegn, hvis tegn er U+0000, så den generiske strenglæser folder den sammen til en tom værdi, og kun den rå kodepoint ved optionsbyte-offsettet bevarer den

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

HotXLS konverterer det nulbaserede XTI-indeks i et BIFF PtgNameX-token til dets enbaserede interne ExternID på ét enkelt punkt, med afgrænset løsning i begge ender og det klassificeringskort, der forbruger det
Off-by-one mellem det nulbaserede diskindeks og det enbaserede interne ExternID anvendes én gang, idet et token kommer ind i syntakstræet, og ethvert uløseligt indeks lander på den misdannede klasse

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