Teknisk artikkel

BIFF SupBook- og XTI-ekstern lenkeklassifisering i Delphi

Åpne en gammel xls, lagre den igjen, og add-in-formelen som kalte inn i et registrert analysebibliotek peker nå på en tom referanse inne i arbeidsboken selv. HotXLS sporer den stille korrupsjonen til én dårlig antakelse: at en BIFF SupBook-post enten er self eller en ekstern fil. [MS-XLS] definerer syv typer, ikke to

Hvorfor mister en lagret arbeidsbok sine add-in-lenker?

Fordi klassifiseringstesten var strukturell i stedet for typet. Den tradisjonelle snarveien leser en SupBook-post ($01AE), sjekker om den bærer self-markøren, og hvis ikke, behandler enhver streng som følger som en dokument-URL. Hver post som er ingen av de to tingene faller gjennom i en standard gren, og standard grenen er nesten alltid at dette er arbeidsboken selv. En add-in støttelenke, en same-sheet-lenke, en ubrukt plass og en avkortet post ender alle opp med å bære samme feil etikett. Ingenting kaster mens dette skjer: posten parset, formelen rekompilerte, filen lagret uten en advarsel, og defekten dukker opp tre uker senere når noen legger merke til en kolonne med nuller der en valutakonvertering pleide å være. [MS-XLS] §2.4.271 beskriver en post som kan være en selvreferanse, en same-sheet-referanse, en add-in funksjonsbeholder, en ekstern arbeidsbok med en virtuell bane og et arknavn-tabell, en DDE- eller OLE-datalenke, eller en ubrukt plassholder — og en syvende tilstand som ikke er i spesifikasjonen men finnes på ekte disker, posten som ikke parser. Fiksen er ikke en bedre heuristikk; den er å nekte å ha en heuristikk i det hele tatt

De syv typene en SupBook-post kan bære

HotXLS deklarerer støttelenke-taksonomien som en lukket opplisting i lxExternSheet.pas, og enhver nedstrøms beslutning bytter på den. Ni opplistingsverdier dekker de syv kategoriene, for DDE- og OLE-tilfellet trenger en foreløpig tilstand før den kan løses:

type
  TXLSSupportingLinkKind = (
    slkUnknown,           // feilet å parse, eller etterfølgende byte gjenstod
    slkSelf,              // denne arbeidsboken
    slkSameSheet,         // U+0000-markør
    slkAddIn,             // add-in funksjonsbeholder
    slkExternalWorkbook,  // virtuell bane + arknavn-tabell
    slkDde,               // løst fra ExternName-flagg
    slkOle,               // løst fra ExternName-flagg
    slkDdeOrOle,          // én av de to, ikke ennå kjent hvilken
    slkUnused);           // enkelt-mellomrom plassholder

  TXLSFormulaReferenceClass = (
    frcInternal,
    frcExternalWorkbook,
    frcExternalOther,
    frcUnknownOrMalformed);

  TXLSXtiInfo = record
    XtiIndex    : Integer;   // null-basert, som lagret i ExternSheet.rgXTI
    ExternID    : Integer;   // én-basert, den interne konvensjonen
    SupBookIndex: Integer;
    Sheet1Index : Integer;
    Sheet2Index : Integer;
    LinkKind    : TXLSSupportingLinkKind;
  end;

Dispatchen er avsender-drevet, ikke streng-drevet. En feltverdi på $0401 markerer self-posten. Et arkantall på én paret med $3A01 markerer en add-in-beholder. Bare en verdi i rekkevidden 1 til $00FF betyr at en kodet virtuell bane følger, og bare da dekoder HotXLS en streng i det hele tatt. Alt utenfor de tre formene forblir slkUnknown, og en post hvis arknavn-tabell ikke konsumerer postkroppen nøyaktig degraderes tilbake til slkUnknown selv når hodet så plausibelt

Den avsender-drevne stigen HotXLS bruker til å klassifisere en BIFF SupBook-post i syv typer, ved å dekode en streng bare for verdier i kodet-bane-rekkevidden og falle tilbake til en ukjent type snarere enn til en standard gren
Hver type nås av en avsender snarere enn av en strengtest, og en post som matcher ingen av formene forblir ukjent i stedet for å falle inn i en standard gren som betyr denne arbeidsboken

Hvorfor dekoder same-sheet-markøren som en tom streng?

Fordi den generelle BIFF strengleseren ødelegger byten klassifiseringen avhenger av. Same-sheet støttelenken er en éntegns streng hvis eneste tegn er U+0000, og TXLSBlob.GetBiffString overleverer den som en tom WideString, uatskillelig fra en genuint tom bane — nøyaktig inndataen en selvreferanse-heuristikk svarer self til. HotXLS leser derfor det rå første kodepunktet ut av postkroppen i stedet for å stole på den dekodede verdien:

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)     // komprimert, é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;

Merk komprimert-mot-bred grenen. Opsjonsbyten sitter ved en fast offset fra strenghodet, og det første kodepunktet er én byte eller to avhengig av bit 0, så å lese den som en byte ubetinget virker på de fleste filer og feiler på de skrevet av lokaliserte bygg — den verste mulige distribusjonen for en feil. Den ubrukte plassholderen fanges på samme måte, ved sin bokstavelige enkelt-mellomrom-nyttelast, og DDE- eller OLE-tilfellet av U+0003-separatoren innebygd i den kodede banen

Hvorfor HotXLS leser det rå første kodepunktet ut av en BIFF SupBook-postkropp i stedet for den dekodede strengen, for den generelle strengleseren gjør same-sheet U+0000-markøren om til en tom verdi
Same-sheet-markøren er en éntegns streng hvis tegn er U+0000, så den generelle strengleseren folder den inn i en tom verdi, og bare det rå kodepunktet ved opsjonsbyte-offseten beholder den

Hvorfor kan ikke DDE og OLE separeres ved SupBook-tid?

Fordi SupBook-posten ikke bærer de skillende bitene. Den forteller deg at lenken er én av de to; fOle- og fOleLink-flaggene som avgjør hvilken bor i ExternName-posten ($0023) som ankommer senere i strømmen. HotXLS registrerer slkDdeOrOle ved parsetid og snevrer den inn i ParseExternalName, og hvis ingen ExternName noensinne ankommer forblir typen foreløpig for alltid — noe som er korrekt, for filen sier det genuint ikke. Hvert forbruker nedstrøms behandler den foreløpige verdien som en ekte verdi snarere enn en manglende, så ingen kaller må finne opp et tiebreak. Å gjette sannsynligvis DDE her ville kjøpe en ryddigere opplisting og en klasse av feil svar ingen kunne spore tilbake:

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 null-baserte på disk og én-baserte inne

HotXLS utfører off-by-one konverteringen nøyaktig én gang, ved punktet et symbol entrer det interne syntakstreet, og ingen andre steder. PtgNameX.ixti ([MS-XLS] §2.5.198.85) er en null-basert indeks inn i rgXTI-matrisen til ExternSheet-posten ($0017, §2.4.106), mens bibliotekets interne ExternID-konvensjon er én-basert med null reservert for ingen eksternt ark. BIFF8-lesebanen gjør FExternID := wValue + 1 når den dekoder et tNameX-symbol og skrivebanen avgir StoreExternID - 1, og lar det rå symbolsynet og on-disk semantikken urørt. Å ta feil på dette er uvanlig vanskelig å fange: eksterne definerte navn løser til nabooppføringen, og i en fil med én enkelt XTI-oppføring blir indeks 0 til indeks 1, bommer, og navnet degraderer stille. En regresjon som bare trener rekompilert formeltekst ser den aldri, for rekompilering rører aldri diskindeksen i det hele tatt — samme felle som gjør definerte navn som spenner ark og arbeidsbøker verdt å teste mot ekte byte-strømmer. Løsning er avgrenset i begge ender: TlxExternSheetSheet.TryResolveXti returnerer False for en negativ indeks eller en manglende oppføring, TXLSSupBook.TryGetKind returnerer False for en SupBook-indeks utenfor matrisen, og ClassifyXti mapper så slkSelf og slkSameSheet til frcInternal, slkExternalWorkbook til frcExternalWorkbook, og slkAddIn, slkDde, slkOle og slkDdeOrOle til frcExternalOther. Alt annet, hver utenfor-rekkevidde-bane inkludert, lander på frcUnknownOrMalformed

HotXLS konverterer den null-baserte XTI-indeksen til et BIFF PtgNameX-symbol til dens én-baserte interne ExternID ved ett enkelt punkt, med avgrenset løsning i begge ender og klassifiseringskartet som konsumerer den
Off-by-one mellom den null-baserte diskindeksen og den én-baserte interne ExternID anvendes én gang, når et symbol entrer syntakstreet, og hver uløselig indeks lander på den feilformede klassen

Klassifisere en formel før den fryses

TXLSCompiledFormula.ClassifyReferences skanner den bevarte BIFF symbolstrømmen direkte i stedet for å dekompilere formelen og søke etter hakeparenteser. Parentes-jakt i formeltekst er en tekstheuristikk iført en parsers frakk: den matcher strengliteraler, den matcher strukturerte referanser, og den bommer helt på eksterne definerte navn, siden de bærer ingen hakeparenteser i dekompilert form. Symbolskanningen ser bare på PtgNameX, PtgRef3d, PtgArea3d, PtgRefErr3d og PtgAreaErr3d, og faller tilbake til en syntakstre-gjennomgang når ingen BIFF-strøm overlever. Sammenfleting er villet pessimistisk — den faste prioriteten er frcUnknownOrMalformed, så frcExternalWorkbook, så frcExternalOther, så frcInternal — så et enkelt uleselig symbol forgifter hele formelen. For et eksternt definert navn valideres navneindeksen også: én-basert, i rekkevidde, og støttet av en beholdt ExternName-post

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 én-basert
    begin
      Sheet := Wb.Sheets[i];
      // fryser BARE formler klassifisert frcExternalWorkbook;
      // interne, add-in, DDE/OLE og feilformede referanser forblir formler
      Sheet.ConvertFormulasToValues(True);
    end;
    Wb.SaveAs('quarterly-detached.xls');
  finally
    Wb.Free;
  end;
end;

OnlyExternal-parameteren er der taksonomien betaler for seg. Å fryse en formel er irreversibelt, så operasjonen må bevise at en referanse er en ekstern arbeidsbok snarere enn bare mistenke det. Add-in-kall overlever, DDE- og OLE-lenker overlever, og alt parseren ikke kunne forstå fullt ut overlever, for det trygge utfallet av usikkerhet er å endre ingenting. Samme disiplin styrer rebinding av formler kopiert mellom arbeidsbøker, der en feilklassifisert referanse re-binder til feil bok i stedet for å feile høyt

Poster som ikke vil parse skrives tilbake urørt

HotXLS beholder den opprinnelige SupBook-nyttelasten og avgir den på nytt byte for byte når posten aldri ble redigert. En parsefeil setter slkUnknown og rydder den deriverte tilstanden, men den fangede kroppen forblir i FRawData og lagringsbanen foretrekker den fremfor enhver rekonstruksjon så lenge elementet ikke er skittent og ikke er self-posten. Alternativet — å normalisere en uparset post inn i en selvreferanse slik at skriveren har noe velformet å avgir — konverterer en post du ikke forsto til en post som er definitivt gal. Det prinsippet er samme kontrakt anvendt på VBA-prosjekter og deres eksterne referanser på tvers av en last-og-lagre-syklus, og det er forskjellen mellom et bibliotek som round-tripper ekte verden filer og et som round-tripper filene testsuiten tilfeldigvis inneholder. En arbeidsbok som har passert gjennom femten år av Excel-versjoner, en rapportgenerator og to migrasjonsverktøy vil inneholde poster ingen nålevende designet. Skriv dem tilbake slik du fant dem

Typet klassifisering av SupBook- og XTI-poster leverte i HotXLS 2.361.2 gjennom 2.361.4, sammen med avgrenset XTI-løsning og den tryggere ConvertFormulasToValues-banen beskrevet her. Vedlikeholder du Delphi- eller C++Builder-kode som leser eldre xls-filer som bærer add-in-kall, DDE- eller OLE-lenker, eller eksterne definerte navn, håndterer HotXLS Delphi spreadsheet component hele taksonomien nativt, uten Excel-installasjon og uten OLE-automatisering på maskinen som gjør arbeidet