Tekninen artikkeli

BIFF SupBook- ja XTI-ulkolinkkien luokittelu Delphissä

Avaa vanha xls, tallenna se uudelleen, ja rekisteröityyn analyysikirjastoon kutsunut lisäosakaava osoittaa nyt tyhjään viitteeseen työkirjan sisällä. HotXLS jäljittää sen hiljaisen vioittumisen yhteen huonoon oletukseen: että BIFF SupBook -tietue on joko itse tai ulkoinen tiedosto. [MS-XLS] määrittelee seitsemän lajia, ei kahta

Miksi tallennettu työkirja menettää lisäosalinkkinsä?

Koska luokittelutesti oli rakenteellinen eikä tyypitetty. Perinteinen oikotie lukee SupBook-tietueen ($01AE), tarkistaa, kantooko se itse-merkintää, ja jos ei, kohtelee mitä tahansa seuraavaa merkkijonoa dokumentin URL:na. Jokainen tietue, joka ei kumpaakaan näistä kahdesta ole, putoaa oletushaaraan, ja oletushaara on lähes aina "tämä on työkirja itse". Lisäosaa tukeva linkki, saman arkin linkki, käyttämätön paikka ja katkaistu tietue päätyvät kaikki kantamaan samaa väärää leimaa. Mikään ei heitä silloin: tietue jäsentyi, kaava käännettiin uudelleen, tiedosto tallennettiin ilman varoitusta, ja vika puhkeaa kolme viikkoa myöhemmin, kun joku huomaa nollasarakkeen siellä, missä valuuttamuunnos ennen oli. [MS-XLS] §2.4.271 kuvaa tietuetta, joka voi olla itseviittaus, saman arkin viittaus, lisäosafunktioiden säiliö, virtuaalipolun ja arkin nimitaulukon omaava ulkoinen työkirja, DDE- tai OLE-datalinkki tai käyttämätön paikkamerkki — ja seitsemännen tilan, jota ei ole määrittelyssä mutta joka on olemassa oikeilla levyillä: tietue, joka ei jäsenny. Korjaus ei ole parempi heuristiikka; se on kieltäytyminen heuristiikasta kokonaan

Seitsemän lajia, jotka SupBook-tietue voi kantaa

HotXLS julistaa tukevien linkkien taksonomian suljettuna luettelointina tiedostossa lxExternSheet.pas, ja jokainen myöhempi päätös kytketään siihen. Yhdeksän luettelointiarvoa kattaa seitsemän kategoriaa, koska DDE- ja OLE-tapaus tarvitsee alustavan tilan ennen kuin se voidaan ratkaista:

type
  TXLSSupportingLinkKind = (
    slkUnknown,           // ei jäsennynyt, tai jälkitavuja jäi jäljelle
    slkSelf,              // tämä työkirja
    slkSameSheet,         // U+0000-merkintä
    slkAddIn,             // lisäosafunktioiden säiliö
    slkExternalWorkbook,  // virtuaalipolku + arkin nimitaulukko
    slkDde,               // ratkaistu ExternName-lipuista
    slkOle,               // ratkaistu ExternName-lipuista
    slkDdeOrOle,          // jompikumpi, ei vielä tiedossa kumpi
    slkUnused);           // yhden välilyönnin paikkamerkki

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

  TXLSXtiInfo = record
    XtiIndex    : Integer;   // nollapohjainen, kuten ExternSheet.rgXTI tallentaa
    ExternID    : Integer;   // ykköspohjainen, sisäinen konventio
    SupBookIndex: Integer;
    Sheet1Index : Integer;
    Sheet2Index : Integer;
    LinkKind    : TXLSSupportingLinkKind;
  end;

Dispatch on sentinelivetoinen, ei merkkijonovetoinen. Kenttäarvo $0401 merkitsee itse-tietuetta. Arkkien määrä yksi paritettuna $3A01-arvoon merkitsee lisäosasäiliötä. Vain arvo välillä 1–$00FF tarkoittaa, että koodattu virtuaalipolku seuraa, ja vasta silloin HotXLS dekoodaa merkkijonoa ylipäätään. Kaikki näiden kolmen muodon ulkopuolinen pysyy slkUnknownina, ja tietue, jonka arkin nimitaulukko ei kuluta tietuerunkoa täsmälleen, alennetaan takaisin slkUnknowniksi vaikka otsikko näytti uskottavalta

Sentinelivetoiset tikapuut, joilla HotXLS luokittelee BIFF SupBook -tietueen seitsemään lajiin, dekoodaten merkkijonon vain koodatun polun alueen arvoille ja pudoten tuntemattomaan lajiin eikä oletushaaraan
Jokaiseen lajiin päädytään sentinelillä eikä merkkijonotestillä, ja tietue, joka ei vastaa mitään muodoista, pysyy tuntemattomana putoamatta oletushaaraan, joka tarkoittaa tätä työkirjaa

Miksi saman arkin merkintä dekoodautuu tyhjäksi merkkijonoksi?

Koska yleiskäyttöinen BIFF-merkkijonolukija tuhoaa tavun, josta luokittelu riippuu. Saman arkin tukeva linkki on yhden merkin merkkijono, jonka ainoa merkki on U+0000, ja TXLSBlob.GetBiffString palauttaa sen tyhjänä WideStringinä, erottamattomana aidosti tyhjästä polusta — mikä on täsmälleen se syöte, johon itseviittausheuristiikka vastaa "self". HotXLS lukee siksi raakan ensimmäisen koodipisteen tietuerungosta sen sijaan, että luottaisi dekoodattuun arvoon:

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)     // pakattu, yksi tavu
  else
    FirstChar := Data.GetWord(StringOffset + 3);    // leveä, kaksi tavua
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;

Kiinnitä huomiota pakatun ja leveän haaraan. Optiotavu istuu kiinteässä siirtymässä merkkijono-otsikon suhteen ja ensimmäinen koodipiste on yksi tavu tai kaksi bitin 0 mukaan, joten sen lukeminen ehdottomasti tavuna toimii useimmilla tiedostoilla ja epäonnistuu lokalisoitujen versioiden kirjoittamilla — huonoin mahdollinen jakauma bugille. Käyttämätön paikkamerkki saadaan kiinni samalla tavalla, sen kirjaimellisesta yhden välilyönnin sisällöstä, ja DDE- tai OLE-tapaus U+0003-erottimesta, joka on upotettu koodattuun polkuun

Miksi HotXLS lukee raakan ensimmäisen koodipisteen BIFF SupBook -tietuerungosta dekoodatun merkkijonon sijaan, koska yleiskäyttöinen merkkijonolukija kääntää saman arkin U+0000-merkinnän tyhjäksi arvoksi
Saman arkin merkintä on yhden merkin merkkijono, jonka merkki on U+0000, joten yleinen merkkijonolukija taittaa sen tyhjäksi arvoksi ja vain raaka koodipiste optiotavun siirtymässä pitää sen

Miksi DDE:tä ja OLE:a ei voi erottaa SupBook-vaiheessa?

Koska SupBook-tietue ei kanna erottavia bittejä. Se kertoo, että linkki on jompikumpi; fOle- ja fOleLink-liput, jotka päättävät kumman, asuvat ExternName-tietueessa ($0023), joka saapuu myöhemmin virrassa. HotXLS kirjaa slkDdeOrOlein jäsennyshetkellä ja kaventaa sen ParseExternalNameissa, ja jos ExternNamea ei koskaan saavu, laji pysyy alustavana ikuisesti — mikä on oikein, koska tiedosto aidosti ei sano sitä. Jokainen myöhempi kuluttaja kohtelee sitä alustavaa arvoa todellisena arvona eikä puuttuvana, joten kukaan kutsujista ei joudu keksimään ratkaisua. Arvaaminen "todennäköisesti DDE" täällä ostaisi siistimmän luetteloinnin ja luokan vääriä vastauksia, joita kukaan ei voisi jäljittää:

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-indeksit ovat nollapohjaisia levyltä ja ykköspohjaisia sisäpuolella

HotXLS suorittaa off-by-one-muunnoksen täsmälleen kerran, sillä hetkellä kun token saapuu sisäiseen syntaksipuuun, eikä missään muualla. PtgNameX.ixti ([MS-XLS] §2.5.198.85) on nollapohjainen indeksi ExternSheet-tietueen ($0017, §2.4.106) rgXTI-taulukkoon, kun taas kirjaston sisäinen ExternID-konventio on ykköspohjainen, nolla varattuna "ei ulkoista arkia" -merkitykseen. BIFF8:n lukupolku tekee FExternID := wValue + 1 dekoodatessaan tNameX-tokenin ja kirjoituspolku tuottaa StoreExternID - 1, jättäen raakan token-näkymän ja levyltä-semantiikan koskemattomiksi. Tämän tekeminen väärin on epätavallisen vaikea saada kiinni: ulkoiset määritellyt nimet ratkeavat naapurimerkintään, ja tiedostossa, jossa on yksi XTI-merkintä, indeksi 0 muuttuu indeksiksi 1, epäonnistuu, ja nimi heikkenee hiljaisesti. Regressio, joka vain harjoittelee uudelleenkäännettyä kaavatekstiä, ei koskaan näe sitä, koska uudelleenkääntäminen ei koskaan kosketa levyn indeksiä — sama ansa, joka tekee arkit ja työkirjat kattavien määriteltyjen nimien testaamisen todellisia tavustreamia vasten arvokkaaksi. Ratkaisu on rajattu molemmissa päissä: TlxExternSheetSheet.TryResolveXti palauttaa False negatiiviselle indeksille tai puuttuvalle merkinnälle, TXLSSupBook.TryGetKind palauttaa False taulukon ulkopuoliselle SupBook-indeksille, ja ClassifyXti kartoittaa sitten slkSelfin ja slkSameSheetin frcInternaliksi, slkExternalWorkbookin frcExternalWorkbookiksi, ja slkAddInin, slkDden, slkOlen ja slkDdeOrOlen frcExternalOtheriksi. Kaikki muu, jokainen alueen ulkopuolinen polku mukaan lukien, laskeutuu frcUnknownOrMalformedille

HotXLS muuntaa BIFF PtgNameX -tokenin nollapohjaisen XTI-indeksin sen ykköspohjaiseksi sisäiseksi ExternID:ksi yhdessä pisteessä, rajatulla ratkaisulla molemmissa päissä ja luokittelukartalla, joka kuluttaa sen
Nollapohjaisen levyindeksin ja ykköspohjaisen sisäisen ExternID:n välinen off-by-one sovelletaan kerran, kun token saapuu syntaksipuuun, ja jokainen ratkaisematon indeksi laskeutuu epämuodostuneeseen luokkaan

Kaavan luokittelu ennen sen jäädyttämistä

TXLSCompiledFormula.ClassifyReferences skannaa säilytetyn BIFF-tokenvirran suoraan sen sijaan, että dekompiloisi kaavan ja etsisi hakasulkeita. Hakasulkeiden metsästys kaavatekstistä on tekstiheuristiikka jäsentäjän takissa: se osuu merkkijonoliteraaleihin, se osuu rakenteellisiin viittauksiin, ja se ohittaa ulkoiset määritellyt nimet kokonaan, koska ne eivät kanna hakasulkeita dekompiloidussa muodossa. Token-skannaus katsoo vain PtgNameXiin, PtgRef3diin, PtgArea3diin, PtgRefErr3diin ja PtgAreaErr3diin, pudoten syntaksipuun kävelyyn, kun BIFF-virta ei säily. Yhdistäminen on tahallisesti pessimistinen — kiinteä prioriteetti on frcUnknownOrMalformed, sitten frcExternalWorkbook, sitten frcExternalOther, sitten frcInternal — joten yksi lukukelvoton token myrkyttää koko kaavan. Ulkoiselle määritellylle nimelle nimen indeksi validoidaan myös: ykköspohjainen, alueella, ja säilytetyn ExternName-tietueen tukema

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 on ykköspohjainen
    begin
      Sheet := Wb.Sheets[i];
      // jäädyttää VAIN frcExternalWorkbookiksi luokitellut kaavat;
      // sisäiset, lisäosa-, DDE/OLE- ja epämuodostuneet viittaukset
      // pysyvät kaavoina
      Sheet.ConvertFormulasToValues(True);
    end;
    Wb.SaveAs('quarterly-detached.xls');
  finally
    Wb.Free;
  end;
end;

OnlyExternal-parametri on paikka, jossa taksonomia maksaa itsensä takaisin. Kaavan jäädyttäminen on peruuttamatonta, joten operaation on todistettava, että viittaus on ulkoinen työkirja, pelkän epäilyn sijaan. Lisäosakutsut selviävät, DDE- ja OLE-linkit selviävät, ja kaikki, mitä jäsentäjä ei voinut täysin ymmärtää, selviää, koska epävarmuuden turvallinen lopputulos on olla muuttamatta mitään. Sama kuri hallitsee työkirjojen välillä kopioiden kaavojen uudelleensidontaa, jossa väärin luokiteltu viittaus sitoutuu uudelleen väärään kirjaan epäonnistumisen sijaan äänekkäästi

Tietueet, jotka eivät jäsenny, kirjoitetaan takaisin koskemattomina

HotXLS säilyttää alkuperäisen SupBook-sisällön ja tuottaa sen uudelleen tavu tavulta, kun tietuetta ei koskaan muokattu. Jäsennysvika asettaa slkUnknownin ja tyhjentää johdetun tilan, mutta kaapattu runko pysyy FRawDatassa ja tallennuspolku suosii sitä minkä tahansa rekonstruktion yli niin kauan kuin nimike ei ole likainen eikä itse-tietue. Vaihtoehto — jäsentymättömän tietueen normalisoiminen itseviittaukseksi, jotta kirjoittajalla on jotain hyvin muodostunutta tuotettavaa — muuntaa tietueen, jota et ymmärtänyt, tietueeksi, joka on lopullisesti väärin. Tuo periaate on sama sopimus, jota sovelletaan VBA-projekteihin ja niiden ulkoisiin viittauksiin lataus-ja-tallennuskierron yli, ja se on ero kirjaston, joka round-tripaa todellisen maailman tiedostoja, ja sellaisen välillä, joka round-tripaa tiedostoja, joita sen testisarja sattuu sisältämään. Työkirja, joka on kulkenut viidentoista vuoden Excel-versioiden, raporttigeneraattorin ja kahden siirtotyökalun läpi, sisältää tietueita, joita kukaan nyt elävistä ei suunnitellut. Kirjoita ne takaisin sellaisina kuin löysit ne

SupBook- ja XTI-tietueiden tyypitetty luokittelu toimitettiin HotXLS 2.361.2–2.361.4:ssä yhdessä rajatun XTI-ratkaisun ja tässä kuvatun turvallisemman ConvertFormulasToValues-polun kanssa. Jos ylläpidät Delphi- tai C++Builder-koodia, joka lukee vanhoja xls-tiedostoja, joissa on lisäosakutsuja, DDE- tai OLE-linkkejä tai ulkoisia määriteltyjä nimiä, HotXLS Delphi spreadsheet component käsittelee koko taksonomian natiivisti, ilman Excel-asennusta ja ilman OLE-automaatiota työtä tekevällä koneella