Tehnički članak

BIFF SupBook i XTI klasifikacija spoljnih veza u Delphi-ju

Otvorite stari xls, sačuvate ga ponovo, i add-in formula koja je pozivala registrovanu analitičku biblioteku sada pokazuje na praznu referencu unutar same radne sveske. HotXLS tu tihu korupciju vraća na jednu lošu pretpostavku: da je BIFF SupBook zapis ili self ili spoljni fajl. [MS-XLS] definiše sedam vrsta, ne dve

Zašto sačuvana radna sveska gubi svoje add-in veze?

Jer je klasifikacioni test bio strukturni umesto tipiziran. Tradicionalna prečica čita SupBook zapis ($01AE), proverava da li nosi self marker, i ako ne, tretira svaki string koji sledi kao URL dokumenta. Svaki zapis koji nije ništa od ta dva pada u podrazumevanu granu, a podrazumevana grana je skoro uvek „ovo je sama radna sveska". Add-in podrška veza, same-sheet veza, nekorišćen slot i odsečen zapis svi završe u istoj pogrešnoj etiketi. Ništa ne baca izuzetak dok se ovo dešava: zapis je parsiran, formula rekompajlirana, fajl sačuvan bez upozorenja, i defekat ispliva tri nedelje kasnije kad neko primeti kolonu nula gde je nekad bila konverzija valute. [MS-XLS] §2.4.271 opisuje zapis koji može biti self-referenca, same-sheet referenca, kontejner add-in funkcija, spoljna radna sveska sa virtuelnom putanjom i tabelom imena listova, DDE ili OLE data veza, ili nekorišćen čuvar mesta — i sedmo stanje koje nije u specifikaciji ali postoji na stvarnim diskovima, zapis koji se ne parsira. Popravka nije bolja heuristika; to je odbijanje da se ima bilo kakva heuristika

Sedam vrsta koje SupBook zapis može nositi

HotXLS deklariše taksonomiju podržavajućih veza kao zatvorenu enumeraciju u lxExternSheet.pas, i svaka dalja odluka se preključuje na nju. Devet vrednosti enumeracije pokriva sedam kategorija, jer DDE i OLE slučaj treba privremeno stanje pre nego što se razreši:

type
  TXLSSupportingLinkKind = (
    slkUnknown,           // parsiranje nije uspelo, ili su ostali prateći bajtovi        
    slkSelf,              // ova radna sveska
    slkSameSheet,         // U+0000 marker
    slkAddIn,             // kontejner add-in funkcija
    slkExternalWorkbook,  // virtuelna putanja + tabela imena listova
    slkDde,               // razrešeno iz ExternName flagova
    slkOle,               // razrešeno iz ExternName flagova
    slkDdeOrOle,          // jedan od dva, još nije poznato koji
    slkUnused);           // čuvar mesta sa jednim razmakom

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

  TXLSXtiInfo = record
    XtiIndex    : Integer;   // nula-baziran, kako je smešten u ExternSheet.rgXTI      
    ExternID    : Integer;   // jedan-baziran, interna konvencija
    SupBookIndex: Integer;
    Sheet1Index : Integer;
    Sheet2Index : Integer;
    LinkKind    : TXLSSupportingLinkKind;
  end;

Raspodela je vođena sentinelima, ne stringovima. Vrednost polja $0401 označava self zapis. Broj listova jedan uparen sa $3A01 označava add-in kontejner. Samo vrednost u opsegu 1 do $00FF znači da sledi kodirana virtuelna putanja, i tek tada HotXLS uopšte dekodira string. Bilo šta van ta tri oblika ostaje slkUnknown, i zapis čija tabela imena listova ne potroši telo zapisa tačno biva vraćen na slkUnknown čak i kada je glava izgledala uverljivo

Sentinelima vođena lestvica koju HotXLS koristi da razvrsta BIFF SupBook zapis u sedam vrsta, dekodirajući string samo za vrednosti u opsegu kodirane putanje i vraćajući se na nepoznatu vrstu umesto na podrazumevanu granu
Svaka vrsta se dostiže sentinelom a ne testom stringa, i zapis koji ne pokriva nijedan oblik ostaje nepoznat umesto da padne u podrazumevanu granu koja znači ova radna sveska

Zašto se same-sheet marker dekodira kao prazan string?

Jer opšti BIFF čitač stringova uništava bajt od koga klasifikacija zavisi. Same-sheet podrška veza je string od jednog znaka čiji je jedini znak U+0000, i TXLSBlob.GetBiffString to vraća kao prazan WideString, nerazlikovljiv od zaista prazne putanje — što je tačno ulaz na koji self-referentna heuristika odgovara „self". HotXLS zato čita sirovi prvi kodni tačak iz tela zapisa umesto da veruje dekodiranoj vrednosti:

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)     // komprimovano, jedan bajt     
  else
    FirstChar := Data.GetWord(StringOffset + 3);    // široko, dva bajta
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;

Obratite pažnju na granu komprimovano-naspram-široko. Opcioni bajt sedi na fiksnom ofsetu od headera stringa, a prvi kodni tačak je jedan ili dva bajta zavisno od bita 0, pa njegovo bezuslovno čitanje kao bajta radi na većini fajlova i pada na onima zapisanim lokalizovanim buildovima — najgore moguća raspodela za bug. Nekorišćen čuvar mesta se hvata na isti način, po svom literalnom sadržaju od jednog razmaka, a DDE ili OLE slučaj po U+0003 separatoru ugrađenom u kodiranu putanju

Zašto HotXLS čita sirovi prvi kodni tačak iz tela BIFF SupBook zapisa umesto dekodiranog stringa, jer opšti čitač stringova pretvara same-sheet U+0000 marker u praznu vrednost
Same-sheet marker je string od jednog znaka čiji je znak U+0000, pa ga opšti čitač stringova presavija u praznu vrednost, i samo sirovi kodni tačak na ofsetu opcionalnog bajta ga čuva

Zašto se DDE i OLE ne mogu razdvojiti u trenutku SupBook-a?

Jer SupBook zapis ne nosi bitove koji razlikuju. On vam kaže da je veza jedna od te dve; fOle i fOleLink flagovi koji odlučuju koji žive u ExternName zapisu ($0023) koji stiže kasnije u toku. HotXLS beleži slkDdeOrOle u trenutku parsiranja i sužava ga u ParseExternalName, i ako nikad ne stigne ExternName vrsta ostaje privremena zauvek — što je ispravno, jer fajl zaista ne kaže. Svaki potrošač nizvodno tretira tu privremenu vrednost kao stvarnu vrednost, a ne kao nedostajuću, pa niko od pozivalaca ne mora da izmišlja razrešnice. Pogađanje „verovatno DDE" ovde bi kupilo uredniju enumeraciju i klasu pogrešnih odgovora koje niko ne bi mogao da prati:

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 indeksi su nula-bazirani na disku i jedan-bazirani unutra

HotXLS izvodi off-by-one konverziju tačno jednom, na tački gde token ulazi u interni sintaksni zapis, i nigde drugde. PtgNameX.ixti ([MS-XLS] §2.5.198.85) je nula-baziran indeks u rgXTI niz ExternSheet zapisa ($0017, §2.4.106), dok je interni ExternID konvencija biblioteke jedan-bazirana sa nulom rezervisanom za „nema spoljnog lista". BIFF8 put čitanja radi FExternID := wValue + 1 kad dekodira tNameX token, a put pisanja ispisuje StoreExternID - 1, ostavljajući sirovi prikaz tokena i semantiku na disku netaknutim. Pogrešiti ovo neobično je teško uhvatiti: spoljna definisana imena se razrešavaju u susedni unos, i u fajlu sa jednim XTI unosom indeks 0 postaje indeks 1, promaši, i ime tiho degeneriše. Regresija koja vežba samo rekompajlirani tekst formule nikad je ne vidi, jer rekompajlacija nikad ne dira disk indeks — ista zamka koja čini definisana imena koja premošćavaju listove i radne sveske vrednim testiranja protiv stvarnih bajt tokova. Razrešavanje je ograničeno sa oba kraja: TlxExternSheetSheet.TryResolveXti vraća False za negativan indeks ili nepostojeći unos, TXLSSupBook.TryGetKind vraća False za SupBook indeks van niza, i ClassifyXti zatim preslikava slkSelf i slkSameSheet na frcInternal, slkExternalWorkbook na frcExternalWorkbook, i slkAddIn, slkDde, slkOle i slkDdeOrOle na frcExternalOther. Sve ostalo, uključujući svaki put van opsega, dospeva na frcUnknownOrMalformed

HotXLS pretvara nula-bazirani XTI indeks BIFF PtgNameX tokena u njegov jedan-bazirani interni ExternID na jednoj tački, sa ograničenim razrešavanjem na oba kraja i mapom klasifikacije koja ga troši
Off-by-one između nula-baziranog disk indeksa i jedan-baziranog internog ExternID primenjuje se jednom, kad token ulazi u sintaksno drvo, i svaki nerazrešiv indeks dospeva na neispravnu klasu

Klasifikacija formule pre nego što se zamrzne

TXLSCompiledFormula.ClassifyReferences skenira sačuvani BIFF token tok direktno umesto da dekompajlira formulu i traži uglaste zagrade. Lov na zagrade u tekstu formule je tekstualna heuristika u parserovom kaputu: poklapa string literale, poklapa struktuirane reference, i spoljna definisana imena potpuno promaši, jer ona u dekompajliranom obliku ne nose zagrade. Token sken gleda samo na PtgNameX, PtgRef3d, PtgArea3d, PtgRefErr3d i PtgAreaErr3d, vraćajući se na obilazak sintaksnog drveta kada BIFF tok ne preživi. Spajanje je namerno pesimistično — fiksni prioritet je frcUnknownOrMalformed, pa frcExternalWorkbook, pa frcExternalOther, pa frcInternal — tako da jedan nečitljiv token otruje celu formulu. Za spoljno definisano ime indeks imena se takođe validira: jedan-baziran, u opsegu, i podupret zadržanim ExternName zapisom

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 je jedan-baziran
    begin
      Sheet := Wb.Sheets[i];
      // zamrzava SAMO formule klasifikovane kao frcExternalWorkbook;
      // interne, add-in, DDE/OLE i neispravne reference ostaju formule       
      Sheet.ConvertFormulasToValues(True);
    end;
    Wb.SaveAs('quarterly-detached.xls');
  finally
    Wb.Free;
  end;
end;

OnlyExternal parametar je mesto gde se taksonomija isplati. Zamrzavanje formule je nepovratno, pa operacija mora dokazati da je referenca spoljna radna sveska, a ne samo posumnjati na to. Add-in pozivi prežive, DDE i OLE veze prežive, i sve što parser nije mogao potpuno da razume preživi, jer je bezbedan ishod neizvesnosti da se ništa ne promeni. Ista disciplina upravlja ponovnim vezivanjem formula kopiranih između radnih sveski, gde pogrešno klasifikovana referenca veže na pogrešnu svesku umesto da glasno padne

Zapisi koji se ne parsiraju vraćaju se netaknuti

HotXLS čuva originalni SupBook sadržaj i ponovo ga ispisuje bajt po bajt kada zapis nikad nije uređivan. Neuspeh parsiranja postavlja slkUnknown i briše izvedeno stanje, ali uhvaćeno telo ostaje u FRawData i put čuvanja mu daje prednost pred svakom rekonstrukcijom dokle je stav neprljav i nije self zapis. Alternativa — normalizacija neparisanog zapisa u self-referencu da bi pisac imao nešto uredno za ispisati — pretvara zapis koji niste razumeli u zapis koji je definitivno pogrešan. Taj princip je isti ugovor primenjen na VBA projekte i njihove spoljne reference kroz ciklus učitavanja i čuvanja, i to je razlika između biblioteke koja stvarne fajlove vraća netaknute i one koja vraća fajlove koje njen test skup slučajno sadrži. Radna sveska koja je prošla kroz petnaest godina Excel verzija, generator izveštaja i dva alata za migraciju sadržaće zapise koje niko živ trenutno nije dizajnirao. Vratite ih onakve kakve ste ih našli

Tipizirana klasifikacija SupBook i XTI zapisa stigla je u HotXLS 2.361.2 do 2.361.4, zajedno sa ograničenim XTI razrešavanjem i sigurnijim ConvertFormulasToValues putem opisanim ovde. Ako održavate Delphi ili C++Builder kod koji čita zastarele xls fajlove sa add-in pozivima, DDE ili OLE vezama ili spoljnim definisanim imenima, HotXLS Delphi spreadsheet component rukuje celom taksonomijom nativno, bez Excel instalacije i bez OLE automatizacije na mašini koja radi posao