Techninis straipsnis

BIFF SupBook ir XTI išorinių nuorodų klasifikacija Delphi

Atidarykite seną xls ir vėl išsaugokite — ir priedo formulė, kuri kvietėsi į užregistruotą analizės biblioteką, dabar rodo į tuščią nuorodą pačios darbo knygos viduje. HotXLS tą nepastebimą sugadinimą priskiria vienai blogai prielaidai: kad BIFF SupBook įrašas yra arba pats save, arba išorinis failas. [MS-XLS] apibrėžia septynias rūšis, o ne dvi

Kodėl išsaugota darbo knyga praranda priedų nuorodas?

Nes klasifikacijos patikra buvo struktūrinė, o ne tipuota. Tradicinis trumpasis kelias perskaito SupBook įrašą ($01AE), patikrina, ar jis nešioja self žymeklį, o jei ne — bet kokią po jo einančią eilutę laiko dokumento URL. Kiekvienas įrašas, kuris nėra nė vienas iš tų dviejų, nukrenta į numatytąją šaką, o numatytoji šaka beveik visada reiškia „tai pati darbo knyga“. Priedų palaikymo nuoroda, to paties lapo nuoroda, nenaudojamas lizdas ir nutrūkęs įrašas visi nešioja tą pačią neteisingą etiketę. Niekas nemeta išimties, kol tai vyksta: įrašas išanalizuotas, formulė perkompiliuota, failas išsaugotas be įspėjimo, o defektas iškyla po trijų savaičių, kai kas nors pastebi nulių stulpelį ten, kur anksčiau buvo valiutos konversija. [MS-XLS] §2.4.271 aprašo įrašą, kuris gali būti savęs-nuoroda, to paties lapo nuoroda, priedo funkcijų konteineris, išorinė darbo knyga su virtualiu keliu ir lapų vardų lentele, DDE ar OLE duomenų nuoroda arba nenaudojamas vietos ženklas — ir septintą būseną, kurios specifikacijoje nėra, bet kuri egzistuoja realiuose diskuose: įrašą, kurio nepavyksta išanalizuoti. Vaistas nėra geresnė heuristika; tai atsisakymas turėti heuristiką apskritai

Septynios rūšys, kurias gali nešioti SupBook įrašas

HotXLS deklaruoja palaikomųjų nuorodų taksonomiją kaip uždarą enumeraciją lxExternSheet.pas, o kiekvienas tolesnis sprendimas jungiasi nuo jos. Devynios enumeracijos reikšmės dengia septynias kategorijas, nes DDE ir OLE atvejui reikia preliminarios būsenos, kol ji dar neišspręsta:

type
  TXLSSupportingLinkKind = (
    slkUnknown,           // nepavyko išanalizuoti arba liko neskaitytų baitų
    slkSelf,              // ši darbo knyga
    slkSameSheet,         // U+0000 žymeklis
    slkAddIn,             // priedo funkcijų konteineris
    slkExternalWorkbook,  // virtualus kelias + lapų vardų lentelė
    slkDde,               // išsprendžiama iš ExternName flagų
    slkOle,               // išsprendžiama iš ExternName flagų
    slkDdeOrOle,          // viena iš dviejų, dar nežinoma kuri
    slkUnused);           // vieno tarpo vietos ženklas

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

  TXLSXtiInfo = record
    XtiIndex    : Integer;   // nuo nulio, kaip saugoma ExternSheet.rgXTI
    ExternID    : Integer;   // nuo vieneto, vidinė konvencija
    SupBookIndex: Integer;
    Sheet1Index : Integer;
    Sheet2Index : Integer;
    LinkKind    : TXLSSupportingLinkKind;
  end;

Maršrutizavimas valdomas kontrolinių reikšmių, o ne eilučių. Lauko reikšmė $0401 žymi self įrašą. Lapų skaičius lygus vienam su $3A01 žymi priedo konteinerį. Tik reikšmė intervale nuo 1 iki $00FF reiškia, jog paskui eina užkoduotas virtualus kelias, ir tik tada HotXLS apskritai dekoduoja eilutę. Viskas už tų trijų formų lieka slkUnknown, o įrašas, kurio lapų vardų lentelė tiksliai nesunaudoja įrašo kūno, nuleidžiamas atgal iki slkUnknown net tada, kai antraštė atrodė įtikinamai

Kontrolinėmis reikšmėmis valdomos kopėčios, kuriomis HotXLS klasifikuoja BIFF SupBook įrašą į septynias rūšis, dekoduoja eilutę tik užkoduoto kelio intervale esančioms reikšmėms ir grįžta į nežinomą rūšį vietoj numatytosios šakos
Kiekviena rūšis pasiekiama kontrole, o ne eilutės testu, ir įrašas, neatitinkantis nė vienos formos, lieka nežinomas vietoj nukritimo į numatytąją šaką, reiškiančią šią darbo knygą

Kodėl to paties lapo žymeklis dekoduojasi į tuščią eilutę?

Nes universalus BIFF eilučių skaitytuvas sunaikina baitą, nuo kurio priklauso klasifikacija. To paties lapo palaikymo nuoroda yra vieno simbolio eilutė, kurios vienintelis simbolis yra U+0000, o TXLSBlob.GetBiffString grąžina ją kaip tuščią WideString, neatskiriamą nuo tikrai tuščio kelio — o tai būtent ta įvestis, į kurią savęs-nuorodos heuristika atsako „self“. HotXLS todėl skaito neapdorotą pirmąjį kodo tašką iš įrašo kūno, o ne pasitiki dekoduota reikšme:

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)     // suspausta, vienas baitas
  else
    FirstChar := Data.GetWord(StringOffset + 3);    // plati, du baitai
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;

Atkreipkite dėmesį į suspaustos-prieš-plačią šaką. Parinkčių baitas sėdi fiksuotu poslinkiu nuo eilutės antraštės, o pirmasis kodo taškas yra vienas arba du baitai, priklausomai nuo bito 0, todėl besąlygiškas jo skaitymas kaip baito veikia daugumoje failų ir kliesta ant tų, kuriuos parašė lokalizuoti variantai — blogiausias galimas klaidos pasiskirstymas. Nenaudojamas vietos ženklas sugaunamas taip pat, pagal jo literalinę vieno tarpo naštą, o DDE ar OLE atvejis — pagal U+0003 skiriklį, įmontuotą į užkoduotą kelią

Kodėl HotXLS skaito neapdorotą pirmąjį kodo tašką iš BIFF SupBook įrašo kūno vietoj dekoduotos eilutės, nes universalus eilučių skaitytuvas to paties lapo U+0000 žymeklį pavercia į tuščią reikšmę
To paties lapo žymeklis yra vieno simbolio eilutė, kurios simbolis yra U+0000, todėl universalus eilučių skaitytuvas jį sulenkia į tuščią reikšmę, ir tik neapdorotas kodo taškas parinkčių baito poslinkyje jį išsaugo

Kodėl DDE ir OLE negali būti atskirti SupBook metu?

Nes SupBook įrašas neneša skiriamųjų bitų. Jis sako, kad nuoroda yra viena iš dviejų; o fOle ir fOleLink flagai, kurie nusprendžia kuri, gyvena ExternName įraše ($0023), ateinančiame vėliau sraute. HotXLS analizės metu užsirašo slkDdeOrOle ir susiaurina jį ParseExternalName, o jei joks ExternName taip ir neatėjo, rūšis amžinai lieka preliminari — ir tai teisinga, nes failas iš tikrųjų to nesako. Kiekvienas žemyn esantis vartotojas tą preliminarią reikšmę traktuoja kaip tikrą, o ne kaip trūkstamą, todėl nė vienas kvietėjas neturi išgalvoti atskyrimo taisyklės. Spėjimas „greičiausiai DDE“ čia nupirktų tvarkingesnę enumeraciją ir visą klasę neteisingų atsakymų, kurių niekas negalėtų atsekti atgal:

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 indeksai diske skaičiuojami nuo nulio, o viduje — nuo vieneto

HotXLS atlieka off-by-one konversiją tiksliai vieną kartą — vietoje, kur tokenas įeina į vidinį sintaksės medį, — ir niekur kitur. PtgNameX.ixti ([MS-XLS] §2.5.198.85) yra nuliu pagrįstas indeksas į ExternSheet įrašo ($0017, §2.4.106) masyvą rgXTI, o bibliotekos vidinė ExternID konvencija yra pagrįsta vienetu, kur nulis rezervuotas „jokio išorinio lapo“. BIFF8 skaitymo kelias daro FExternID := wValue + 1, kai dekoduoja tNameX tokeną, o rašymo kelias išduoda StoreExternID - 1, palikdamas žalią tokeno vaizdą ir disko semantiką nepaliestus. Suklysti čia neįprastai sunku pagauti: išoriniai apibrėžtieji vardai išsprendžia į gretimą įrašą, o faile su vieninteliu XTI įrašu indeksas 0 tampa 1, praleidžia, ir vardas tyliai suprastėja. Regresija, kuri išbando tik perkompiliuotą formulės tekstą, niekada to nemato, nes perkompiliavimas visai neliečia disko indekso — tie patys spąstai, dėl kurių lapus ir darbo knygas apimančius apibrėžtuosius vardus verta testuoti prieš tikras baitų srautus. Išsprendimas apribotas abiejuose galuose: TlxExternSheetSheet.TryResolveXti grąžina False neigiamam indeksui arba trūkstamam įrašui, TXLSSupBook.TryGetKind grąžina False SupBook indeksui už masyvo ribų, o ClassifyXti tada susieja slkSelf ir slkSameSheet su frcInternal, slkExternalWorkbook su frcExternalWorkbook, o slkAddIn, slkDde, slkOle ir slkDdeOrOle su frcExternalOther. Viskas kita, įskaitant kiekvieną už ribų esantį kelią, nukrenta ant frcUnknownOrMalformed

HotXLS konvertuoja BIFF PtgNameX tokeno nuliu pagrįstą XTI indeksą į jo vienetu pagrįstą vidinį ExternID viename taške, su apribotu išsprendimu abiejuose galuose ir klasifikacijos susiejimu, kuris jį vartoja
Off-by-one tarp nuliu pagrįsto disko indekso ir vienetu pagrįsto vidinio ExternID taikomas vieną kartą, kai tokenas įeina į sintaksės medį, ir kiekvienas neišsprendžiamas indeksas nukrenta ant sugadintos klasės

Formulės klasifikavimas prieš jos užšaldymą

TXLSCompiledFormula.ClassifyReferences skaito išsaugotą BIFF tokenų srautą tiesiogiai, vietoj to, kad dekompiliuotų formulę ir ieškotų kvadratinių skliaustų. Skliaustų medžioklė formulės tekste yra tekstinė heuristika, vilkinti analizatoriaus paltą: ji sutampa su eilučių literalais, ji sutampa su struktūrinėmis nuorodomis, ir ji visiškai praleidžia išorinius apibrėžtuosius vardus, nes tie dekompiliuota forma nesineša jokių skliaustų. Tokenų skenavimas žiūri tik į PtgNameX, PtgRef3d, PtgArea3d, PtgRefErr3d ir PtgAreaErr3d, o kai BIFF srautas neišgyvena, grįžta prie sintaksės medžio perėjimo. Suliejimas tyčia pesimistiškas — fiksuotas prioritetas yra frcUnknownOrMalformed, paskui frcExternalWorkbook, paskui frcExternalOther, paskui frcInternal — todėl vienas neskaitytas tokenas apnuodija visą formulę. Išoriniam apibrėžtajam vardui patvirtinamas ir vardo indeksas: pagrįstas vienetu, intervale ir remiamas išsaugoto ExternName įrašo

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 skaičiuojamas nuo vieneto
    begin
      Sheet := Wb.Sheets[i];
      // užšaldo TIK formules, klasifikuotas frcExternalWorkbook;
      // vidinės, priedų, DDE/OLE ir sugadintos nuorodos lieka formulėmis
      Sheet.ConvertFormulasToValues(True);
    end;
    Wb.SaveAs('quarterly-detached.xls');
  finally
    Wb.Free;
  end;
end;

OnlyExternal parametras yra vieta, kur taksonomija atsipirčia. Formulės užšaldymas yra neatšaukiamas, todėl operacija turi įrodyti, kad nuoroda yra išorinė darbo knyga, o ne tik įtarti tai. Priedų iškvietimai išgyvena, DDE ir OLE nuorodos išgyvena, ir visa, ko analizatorius negalėjo pilnai suprasti, išgyvena, nes neapibrėžtumo saugus rezultatas yra nieko nekeisti. Ta pati disciplina valdo formulių, kopijuojamų tarp darbo knygų, persiejimą, kur neteisingai klasifikuota nuoroda persiejasi prie neteisingos knygos vietoj garsaus gedimo

Įrašai, kurių nepavyksta išanalizuoti, rašomi atgal nepaliesti

HotXLS laiko originalią SupBook naštą ir iš naujo išduoda ją baitas po baito, kai įrašas niekada nebuvo redaguotas. Analizės nesėkmė nustato slkUnknown ir išvalo išvestinę būseną, bet pagautas kūnas lieka FRawData, o saugojimo kelias teikia jam pirmenybę prieš bet kokį atkūrimą, kol elementas nėra dirty ir nėra self įrašas. Alternatyva — nenuskaitomą įrašą suvienodinti iki savęs-nuorodos, kad rašytojas turėtų ką gerai suformuoto išduoti — pavercia įrašą, kurio nesupratote, į įrašą, kuris yra neabejotinai neteisingas. Tas principas yra tas pats kontraktas, taikomas VBA projektams ir jų išorinėms nuorodams per įkėlimo-ir-išsaugojimo ciklą, ir jis yra skirtumas tarp bibliotekos, kuri perkelia realaus pasaulio failus, ir vienos, kuri perkelia failus, kuriuos jos testų rinkinys nutiko turėti. Darbo knyga, praėjusi per penkiolika Excel versijų metų, ataskaitų generatorių ir du migracijos įrankius, turės įrašų, kurių niekas gyvas nesudarė. Rašykite juos atgal taip, kaip radote

Tipuota SupBook ir XTI įrašų klasifikacija atkeliavo HotXLS 2.361.2–2.361.4 versijose, kartu su apribotu XTI išsprendimu ir čia aprašytu saugesniu ConvertFormulasToValues keliu. Jei prižiūrite Delphi ar C++Builder kodą, skaitantį senus xls failus su priedų iškvietimais, DDE ar OLE nuorodomis arba išoriniais apibrėžtaisiais vardais, HotXLS Delphi skaičiuoklių komponentas apdoroja visą taksonomiją natyviai, be Excel įdiegimo ir be OLE automatizavimo mašinoje, atliekančioje darbą