Műszaki cikk

BIFF SupBook és XTI külső hivatkozás-osztályozás Delphiben

Nyisson meg egy régi xls-t, mentse el újra, és a regisztrált elemzőkönyvtárba hívó bővítményképlet most a munkafüzet belsejében lévő üres hivatkozásra mutat. A HotXLS ezt a csendes sérülést egyetlen rossz feltételezésre vezeti vissza: arra, hogy egy BIFF SupBook rekord vagy self, vagy külső fájl. A [MS-XLS] hét fajtát definiál, nem kettőt

Miért veszti el egy mentett munkafüzet a bővítményhivatkozásait?

Mert az osztályozási teszt strukturális volt, nem típusos. A hagyományos rövidített út beolvassa a SupBook rekordot ($01AE), megvizsgálja, hogy hordozza-e a self jelzőt, és ha nem, a következő bármilyen sztringet dokumentum URL-ként kezeli. Minden rekord, amely egyik sem a kettő közül, egy alapértelmezett ágba esik, és az alapértelmezett ág majdnem mindig az, hogy ez maga a munkafüzet. Egy bővítmény-támogató hivatkozás, egy azonos munkalapos hivatkozás, egy nem használt hely és egy csonkolt rekord mind ugyanazt a rossz címkét kapja. Semmi nem dob közben: a rekord értelmezve lett, a képlet újrafordult, a fájl figyelmeztetés nélkül mentődött, és a hiba három héttel később kerül felszínre, amikor valaki nullák oszlopát veszi észre ott, ahol valaha devizaátváltás volt. A [MS-XLS] §2.4.271 olyan rekordot ír le, amely lehet önreferencia, azonos munkalapos hivatkozás, bővítményfüggvény-tároló, virtuális útvonalas és munkalapnév-táblás külső munkafüzet, DDE vagy OLE adatkapcsolat, vagy nem használt helyőrző — és egy hetedik állapot, amely nem szerepel a specifikációban, de valós lemezeken létezik, a nem értelmezhető rekord. A javítás nem egy jobb heurisztika; az, hogy egyáltalán nem enged heurisztikát

A hét fajta, amelyet egy SupBook rekord hordozhat

A HotXLS a támogató hivatkozások taxonómiáját zárt felsorolásként deklarálja a lxExternSheet.pas-ban, és minden lefelé áramló döntés erre kapcsol. Kilenc felsorolási érték fedi le a hét kategóriát, mert a DDE és OLE esetnek ideiglenes állapotra van szüksége, mielőtt feloldhatóvá válna:

type
  TXLSSupportingLinkKind = (
    slkUnknown,           // nem értelmezhető, vagy maradtak záró bájtok
    slkSelf,              // ez a munkafüzet
    slkSameSheet,         // U+0000 jelző
    slkAddIn,             // bővítményfüggvény-tároló
    slkExternalWorkbook,  // virtuális útvonal + munkalapnév-tábla
    slkDde,               // az ExternName jelzőkből oldódik fel
    slkOle,               // az ExternName jelzőkből oldódik fel
    slkDdeOrOle,          // a kettő egyike, még nem ismert, melyik
    slkUnused);           // egyetlen szóközből álló helyőrző

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

  TXLSXtiInfo = record
    XtiIndex    : Integer;   // nulla alapú, ahogy az ExternSheet.rgXTI tárolja
    ExternID    : Integer;   // egy alapú, a belső konvenció
    SupBookIndex: Integer;
    Sheet1Index : Integer;
    Sheet2Index : Integer;
    LinkKind    : TXLSSupportingLinkKind;
  end;

Az elágazás sentinel-vezérelt, nem sztringvezérelt. A $0401 mezőérték a self rekordot jelöli. Az egy munkalapszám, $3A01-gyel párosítva, bővítménytárolót jelöl. Csak az 1-től $00FF-ig terjedő tartományba eső érték jelent kódolt virtuális útvonalat, és csak akkor dekódol a HotXLS egyáltalán sztringet. Minden, ami e három alakon kívül esik, slkUnknown marad, és az a rekord, amelynek munkalapnév-táblája nem fogyasztja el pontosan a rekord törzsét, visszaminősül slkUnknown-ra akkor is, ha a feje hihetőnek tűnt

A sentinel-vezérelt létra, amelyet a HotXLS a BIFF SupBook rekord hét fajtára osztályozásához használ, csak a kódolt útvonal tartományába eső értékeknél dekódol sztringet, és ismeretlen fajtára esik vissza, nem alapértelmezett ágba
Minden fajtát sentinel ér el, nem sztringteszt, és a valamelyik alakra sem illeszkedő rekord ismeretlen marad, ahelyett hogy abba az alapértelmezett ágba esne, amely azt jelenti, hogy ez a munkafüzet

Miért dekódolódik az azonos munkalap jelző üres sztringként?

Mert az általános célú BIFF sztringolvasó tönkretesi azt a bájtot, amelytől az osztályozás függ. Az azonos munkalapos támogató hivatkozás egyetlen karakteres sztring, amelynek egyetlen karaktere U+0000, és a TXLSBlob.GetBiffString ezt üres WideString-ként adja vissza, megkülönböztethetetlenül egy valóban üres útvonaltól — ami pontosan az a bemenet, amelyre egy önreferencia-heurisztika azt feleli, self. A HotXLS ezért a rekord törzséből olvassa ki az első nyers kódpontot, ahelyett hogy a dekódolt értékben bízna:

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)     // tömörített, egy bájt
  else
    FirstChar := Data.GetWord(StringOffset + 3);    // széles, két bájt
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;

Vegye észre a tömörített-versus-széles ágat. Az opcióbájt a sztring fejlécétől rögzített eltolásban ül, és az első kódpont a 0 bit értékétől függően egy vagy két bájt, így feltétel nélküli bájtként való olvasása a legtöbb fájlon működik, és azokon hibázik, amelyeket honosított buildek írtak — ez a hiba számára a lehető legrosszabb eloszlás. A nem használt helyőrző ugyanígy kapja el, a szó szerinti egyetlen szóközből álló tartalma alapján, a DDE vagy OLE eset pedig a kódolt útvonalba ágyazott U+0003 elválasztó alapján

Miért olvassa a HotXLS az első nyers kódpontot a BIFF SupBook rekord törzséből a dekódolt sztring helyett, mert az általános célú sztringolvasó az azonos munkalapos U+0000 jelzőt üres értékké hajtja össze
Az azonos munkalapos jelző egyetlen karakteres sztring, amelynek karaktere U+0000, így az általános sztringolvasó üres értékké hajtja össze, és csak az opcióbájt eltolásánál lévő nyers kódpont őrzi meg

Miért nem bontható szét a DDE és az OLE a SupBook elemzésekor?

Mert a SupBook rekord nem hordozza a megkülönböztető biteket. Megmondja, hogy a hivatkozás a kettő egyike; a fOle és fOleLink jelzők, amelyek eldöntik, hogy melyik, a streamben később érkező ExternName rekordban ($0023) élnek. A HotXLS elemzéskor slkDdeOrOle-t jegyez fel, és a ParseExternalName-ben szűkíti, és ha sosem érkezik ExternName, a fajta örökre ideiglenes marad — ami helyes, mert a fájl valóban nem mondja meg. Minden lefelé áramló fogyasztó valós értékként kezeli azt az ideiglenes értéket, nem hiányzóként, így egyetlen hívónak sem kell döntőbírálatot kitalálnia. A valószínűleg DDE találgatása itt rendezettebb felsorolást vásárolna, és egy olyan téves válaszosztályt, amelyet senki sem tudna visszavezetni:

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;

Az XTI indexek a lemezen nulla alapúak, belül egy alapúak

A HotXLS az off-by-one konverziót pontosan egyszer hajtja végre, azon a ponton, ahol egy token belép a belső szintaxisfába, és sehol máshol. A PtgNameX.ixti ([MS-XLS] §2.5.198.85) nulla alapú index az ExternSheet rekord ($0017, §2.4.106) rgXTI tömbjébe, míg a könyvtár belső ExternID konvenciója egy alapú, ahol a nulla a külső munkalap hiányát jelenti. A BIFF8 olvasási útvonal FExternID := wValue + 1-t hajt végre, amikor egy tNameX tokent dekódol, az írási útvonal pedig StoreExternID - 1-et bocsát ki, érintetlenül hagyva a nyers token nézetet és a lemezen lévő szemantikát. Ezt elrontani szokatlanul nehéz észrevenni: a külső definiált nevek a szomszédos bejegyzésre oldódnak fel, és egyetlen XTI bejegyzésű fájlban a 0 index 1-es indexszé válik, nem talál, és a név csendben degradálódik. Egy regresszió, amely csak újrafordított képletszöveget mozgat meg, sosem látja, mert az újrafordítás sosem nyúl a lemezen lévő indexhez — ugyanaz a csapda, amely miatt a munkalapokon és munkafüzeteken átívelő definiált neveket érdemes valódi bájtstreamen tesztelni. A feloldás mindkét végén határolt: a TlxExternSheetSheet.TryResolveXti False-t ad negatív indexre vagy hiányzó bejegyzésre, a TXLSSupBook.TryGetKind False-t ad a tömbön kívüli SupBook indexre, a ClassifyXti pedig ezután a slkSelf-et és a slkSameSheet-et frcInternal-re, a slkExternalWorkbook-ot frcExternalWorkbook-ra, a slkAddIn-t, slkDde-t, slkOle-t és slkDdeOrOle-t frcExternalOther-re képezi le. Minden más, minden tartományon kívüli útvonal egyaránt frcUnknownOrMalformed-ra landol

A HotXLS átalakítja a BIFF PtgNameX token nulla alapú XTI indexét egy alapú belső ExternID-vé egyetlen ponton, mindkét végén határolt feloldással és az azt fogyasztó osztályozási leképezéssel
A nulla alapú lemez index és az egy alapú belső ExternID közti off-by-one egyszer alkalmazódik, ahogy a token belép a szintaxisfába, és minden fel nem oldható index a hibás formájú osztályra landol

Képlet osztályozása a befagyasztása előtt

A TXLSCompiledFormula.ClassifyReferences közvetlenül a megőrzött BIFF token streamet pásztázza, ahelyett hogy dekompilálná a képletet, és szögletes zárójeleket keresne. A zárójelvadászat a képletszövegben szöveges heurisztika elemzőkabátban: sztringliterálokra illeszkedik, strukturált hivatkozásokra illeszkedik, és a külső definiált neveket teljesen kihagyja, mert azok dekompilált alakban nem hordoznak zárójelet. A tokenpásztázás csak a PtgNameX-re, PtgRef3d-re, PtgArea3d-re, PtgRefErr3d-re és PtgAreaErr3d-re néz, szintaxisfa-járásként esve vissza, amikor BIFF stream nem él túl. Az egyesítés szándékosan pesszimista — a rögzített prioritás frcUnknownOrMalformed, azután frcExternalWorkbook, aztán frcExternalOther, végül frcInternal —, így egyetlen olvashatatlan token megmérgezi az egész képletet. Külső definiált névnél a névindex is validálásra kerül: egy alapú, tartományon belüli, és megőrzött ExternName rekord által megtámasztott

var
  Wb   : TXLSWorkbook;
  Sheet: TXLSWorksheet;
  i    : Integer;
begin
  Wb := TXLSWorkbook.Create;
  try
    Wb.Open('quarterly.xls');
    for i := 1 to Wb.Sheets.Count do        // a Sheets egy alapú
    begin
      Sheet := Wb.Sheets[i];
      // CSAK a frcExternalWorkbook osztályú képleteket fagyasztja meg;
      // a belső, bővítmény-, DDE/OLE és hibás alakú hivatkozások képletek maradnak
      Sheet.ConvertFormulasToValues(True);
    end;
    Wb.SaveAs('quarterly-detached.xls');
  finally
    Wb.Free;
  end;
end;

A OnlyExternal paraméter az a hely, ahol a taxonómia kifizeti magát. Egy képlet befagyasztása visszafordíthatatlan, ezért a műveletnek bizonyítania kell, hogy egy hivatkozás külső munkafüzet, nem pedig pusztán gyanítania. A bővítményhívások túlélik, a DDE és OLE kapcsolatok túlélik, és minden, amit az elemző nem tudott teljesen megérteni, túléli, mert a bizonytalanság biztonságos kimenete az, hogy semmit nem változtat. Ugyanez a fegyelem irányítja a munkafüzetek között másolt képletek újrakötését is, ahol egy rosszul osztályozott hivatkozás a rossz munkafüzetre köt át, hangos hibajelzés helyett

A nem értelmezhető rekordok érintetlenül íródnak vissza

A HotXLS megtartja az eredeti SupBook hasznos terhét, és bájtonként adja ki újra, ha a rekordot sosem szerkesztették. Egy értelmezési hiba slkUnknown-ot állít be, és törli a levezetett állapotot, de a rögzített törzs megmarad a FRawData-ban, és a mentési útvonal ezt részesíti előnyben bármilyen rekonstrukcióval szemben, amíg az elem nem dirty, és nem a self rekord. Az alternatíva — egy nem értelmezett rekord önreferenciává normalizálása, hogy az írónak legyen jól formált kibocsátanivalója — egy olyan rekordot alakít véglegesen rossz rekorddá, amelyet nem értett. Ez az elv ugyanaz a szerződés, amelyet a VBA projektek és a külső hivatkozásaik kapnak meg egy betöltési-mentési ciklus során, és ez a különbség egy olyan könyvtár között, amely a valós fájlokat is érintetlenül juttatja oda-vissza, és egy olyan között, amely csak a saját tesztkészletében véletlenül jelen lévő fájlokat. Egy munkafüzet, amely tizenöt évnyi Excel verzión, egy jelentésgenerátoron és két migrációs eszközön ment keresztül, olyan rekordokat fog tartalmazni, amelyeket jelenleg egyetlen élő ember sem tervezett. Írja vissza őket úgy, ahogy találta

A SupBook és XTI rekordok típusos osztályozása a HotXLS 2.361.2-től 2.361.4-ig terjedő verziókban érkezett, a határolt XTI feloldással és az itt leírt biztonságosabb ConvertFormulasToValues útvonallal együtt. Ha olyan Delphi vagy C++Builder kódot tart karban, amely bővítményhívásokat, DDE vagy OLE kapcsolatokat vagy külső definiált neveket hordozó legacy xls fájlokat olvas, a HotXLS Delphi táblázatkezelő komponens natívan kezeli a teljes taxonómiát, Excel telepítés és OLE automatizálás nélkül azon a gépen, amely a munkát végzi