Tehnički članak

Klasifikacija BIFF SupBook i XTI vanjskih veza u Delphiju

Otvorite stari xls, spremite ga ponovno, i add-in formula koja je pozivala registriranu analitičku biblioteku sada upućuje na praznu referencu unutar same radne knjige. HotXLS taj tih kvar prati do jedne loše pretpostavke: da je BIFF SupBook zapis ili self ili vanjska datoteka. [MS-XLS] definira sedam vrsta, a ne dvije

Zašto spremljena radna knjiga izgubi svoje add-in veze?

Zato što je klasifikacijski test bio strukturni umjesto tipiziran. Tradicionalni prečac čita SupBook zapis ($01AE), provjerava nosi li self oznaku i ako ne, tretira sve što slijedi kao URL dokumenta. Svaki zapis koji nije ni jedno ni drugo propada u zadanu granu, a zadana grana je gotovo uvijek "ovo je sama radna knjiga". Add-in potporna veza, veza na isti list, nekorišteno mjesto i odrezan zapis svi na kraju nose istu pogrešnu etiketu. Ništa ne baca iznimku dok se ovo događa: zapis je parsiran, formula ponovno kompajlirana, datoteka spremljena bez upozorenja, a defekt izbije tri tjedna kasnije kad netko primijeti stupac nula tamo gdje je nekad bila pretvorba valute. [MS-XLS] §2.4.271 opisuje zapis koji može biti samo-referenca, referenca na isti list, spremnik add-in funkcija, vanjska radna knjiga s virtualnom stazom i tablicom imena listova, DDE ili OLE podatkovna veza ili nekorišteno rezervirano mjesto — i sedmo stanje koje nije u specifikaciji ali postoji na stvarnim diskovima: zapis koji se ne da parsirati. Rješenje nije bolja heuristika; to je odbijanje imati heuristiku uopće

Sedam vrsta koje SupBook zapis može nositi

HotXLS deklarira taksonomiju potpornih veza kao zatvoreno nabrajanje u lxExternSheet.pas, i svaka odluka nizvodno prelazi na nju. Devet vrijednosti nabrajanja pokriva sedam kategorija, jer DDE i OLE slučaj treba privremeno stanje prije nego što se razriješi:

type
  TXLSSupportingLinkKind = (
    slkUnknown,           // parsiranje propalo, ili su ostali bajtovi na kraju
    slkSelf,              // ova radna knjiga
    slkSameSheet,         // oznaka U+0000
    slkAddIn,             // spremnik add-in funkcija
    slkExternalWorkbook,  // virtualna staza + tablica imena listova
    slkDde,               // razriješeno iz ExternName zastavica
    slkOle,               // razriješeno iz ExternName zastavica
    slkDdeOrOle,          // jedno od dviju, još se ne zna koje
    slkUnused);           // rezervirano mjesto od jednog razmaka

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

  TXLSXtiInfo = record
    XtiIndex    : Integer;   // s bazom nula, kako je spremljeno u ExternSheet.rgXTI
    ExternID    : Integer;   // s bazom jedan, interna konvencija
    SupBookIndex: Integer;
    Sheet1Index : Integer;
    Sheet2Index : Integer;
    LinkKind    : TXLSSupportingLinkKind;
  end;

Raspodjela upravlja se stražarnim vrijednostima, a ne nizovima. Vrijednost polja $0401 označava self zapis. Broj listova jedan uparen s $3A01 označava add-in spremnik. Samo vrijednost u rasponu 1 do $00FF znači da slijedi kodirana virtualna staza, i tek tada HotXLS uopće dekodira niz. Sve izvan ta tri oblika ostaje slkUnknown, i zapis čija tablica imena listova ne troši tijelo zapisa točno spušta se natrag na slkUnknown čak i kada je glava izgledala uvjerljivo

Ljestva kojima HotXLS uz pomoć stražarnih vrijednosti klasificira BIFF SupBook zapis u sedam vrsta, dekodirajući niz samo za vrijednosti u rasponu kodirane staze i vraćajući se na nepoznatu vrstu umjesto na zadanu granu
Do svake vrste dolazi se stražarnom vrijednošću a ne testom niza, i zapis koji ne odgovara nijednom obliku ostaje nepoznat umjesto da upadne u zadanu granu koja znači ova radna knjiga

Zašto se oznaka istog lista dekodira kao prazan niz?

Zato što opći BIFF čitač nizova uništava bajt od kojeg klasifikacija ovisi. Potporna veza na isti list jest niz od jednog znaka čiji je jedini znak U+0000, i TXLSBlob.GetBiffString to vraća kao prazan WideString, nerazlučiv od doista prazne staze — a to je točno ulaz na koji heuristika samo-reference odgovara "self". HotXLS zato čita sirovu prvu kodnu točku iz tijela zapisa umjesto da vjeruje dekodiranoj vrijednosti:

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)     // komprimirano, 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 pozornost na granu komprimirano-naspram-široko. Bajt opcija sjedi na fiksnom pomaku od zaglavlja niza i prva kodna točka ima jedan ili dva bajta ovisno o bitu 0, pa je čitanje kao bajta bezuvjetno radi na većini datoteka a propada na onima zapisanim lokaliziranim izdanjima — najgora moguća raspodjela za grešku. Nekorišteno rezervirano mjesto hvata se na isti način, po svom doslovnom sadržaju od jednog razmaka, a DDE ili OLE slučaj po U+0003 razdjelniku ugrađenom u kodiranu stazu

Zašto HotXLS čita sirovu prvu kodnu točku iz tijela BIFF SupBook zapisa umjesto dekodiranog niza, jer opći čitač nizova pretvara oznaku istog lista U+0000 u praznu vrijednost
Oznaka istog lista jest niz od jednog znaka čiji je znak U+0000, pa je opći čitač nizova stapa u praznu vrijednost i samo sirova kodna točka na pomaku bajta opcija čuva

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

Zato što SupBook zapis ne nosi razlučujuće bitove. On vam kaže da je veza jedno od dvaju; zastavice fOle i fOleLink koje odlučuju koja žive u ExternName zapisu ($0023) koji stiže kasnije u toku. HotXLS bilježi slkDdeOrOle u trenutku parsiranja i sužava ga u ParseExternalName, i ako ExternName nikada ne stigne, vrsta ostaje privremena zauvijek — što je ispravno, jer datoteka doista ne kaže. Svaki potrošač nizvodno tretira tu privremenu vrijednost kao stvarnu vrijednost a ne kao nedostajuću, pa nijedan pozivatelj ne mora izmišljati razrješavanje neriješenog. Pogađanje "vjerojatno DDE" ovdje kupilo bi urednije nabrajanje i klasu pogrešnih odgovora koje nitko ne bi mogao pratiti unatrag:

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 na disku imaju bazu nula, a iznutra bazu jedan

HotXLS izvodi konverziju pomaka baze točno jednom, u trenutku kad token ulazi u interno sintaksno stablo, i nigdje drugdje. PtgNameX.ixti ([MS-XLS] §2.5.198.85) jest indeks s bazom nula u polje rgXTI zapisa ExternSheet ($0017, §2.4.106), dok je interna konvencija ExternID biblioteke s bazom jedan, uz nulu rezerviranu za "nema vanjskog lista". BIFF8 put čitanja radi FExternID := wValue + 1 kada dekodira token tNameX, a put pisanja ispisuje StoreExternID - 1, ostavljajući sirovi prikaz tokena i semantiku na disku netaknutima. Ovo pogriješiti neobično je teško uhvatiti: vanjska definirana imena riješe se na susjedni unos, i u datoteci s jednim XTI unosom indeks 0 postaje indeks 1, promaši, i ime se tiho degradira. Regresija koja vježba samo ponovno kompajlirani tekst formula nikada je ne vidi, jer ponovna kompilacija nikada ne dira indeks na disku — ista zamka koja čini definirana imena koja se protežu preko listova i radnih knjiga vrijednim testiranja protiv stvarnih bajtovskih tokova. Razrješavanje je ograničeno na oba kraja: TlxExternSheetSheet.TryResolveXti vraća False za negativan indeks ili nedostajući unos, TXLSSupBook.TryGetKind vraća False za SupBook indeks izvan polja, i ClassifyXti zatim preslikava slkSelf i slkSameSheet na frcInternal, slkExternalWorkbook na frcExternalWorkbook, a slkAddIn, slkDde, slkOle i slkDdeOrOle na frcExternalOther. Sve ostalo, uključujući svaki put izvan raspona, slijeće na frcUnknownOrMalformed

HotXLS pretvara XTI indeks s bazom nula BIFF tokena PtgNameX u njegov interni ExternID s bazom jedan u jednoj točki, uz ograničeno razrješavanje na oba kraja i kartu klasifikacije koja ga troši
Pomak baze između indeksa na disku s bazom nula i internog ExternID-a s bazom jedan primjenjuje se jednom, kad token ulazi u sintaksno stablo, i svaki nerješivi indeks slijeće na klasu neispravnog

Klasifikacija formule prije njezina zamrzavanja

TXLSCompiledFormula.ClassifyReferences izravno pretražuje sačuvani BIFF tok tokena umjesto da dekompilira formulu i traži uglate zagrade. Lov na zagrade u tekstu formule je tekstualna heuristika u ogrtaču parsera: poklapa string literale, poklapa strukturirane reference i u potpunosti promaši vanjska definirana imena, jer ona u dekompiliranoj formi ne nose zagrade. Pretraga tokena gleda samo PtgNameX, PtgRef3d, PtgArea3d, PtgRefErr3d i PtgAreaErr3d, s povratkom na prolaz sintaksnog stabla kada BIFF tok ne preživi. Spajanje je namjerno pesimistično — fiksni prioritet jest frcUnknownOrMalformed, zatim frcExternalWorkbook, zatim frcExternalOther, zatim frcInternal — pa jedan nečitljiv token otrova cijelu formulu. Za vanjsko definirano ime ovjerava se i indeks imena: s bazom jedan, u rasponu i poduprt 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 ima bazu jedan
    begin
      Sheet := Wb.Sheets[i];
      // zamrzava SAMO formule klasificirane 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;

Parametar OnlyExternal mjesto je gdje se taksonomija isplati. Zamrzavanje formule je nepovratno, pa operacija mora dokazati da je referenca vanjska radna knjiga umjesto da je tek posumnja. Add-in pozivi prežive, DDE i OLE veze prežive, i sve što parser nije mogao potpuno razumjeti preživi, jer je siguran ishod nesigurnosti ne mijenjati ništa. Ista disciplina upravlja ponovnim vezivanjem formula kopiranih između radnih knjiga, gdje se pogrešno klasificirana referenca veže na krivu knjigu umjesto da glasno propadne

Zapisi koji se ne daju parsirati vraćaju se netaknuti

HotXLS čuva izvorni SupBook sadržaj i ponovno ga emitira bajt po bajt kada zapis nikada nije uređivan. Propast parsiranja postavlja slkUnknown i briše izvedeno stanje, ali uhvaćeno tijelo ostaje u FRawData i put spremanja preferira ga nad svakom rekonstrukcijom dok god stavka nije prljava i nije self zapis. Alternativa — normalizacija neparsiranog zapisa u samo-referencu kako bi pisac imao nešto dobro oblikovano za ispisati — pretvara zapis koji niste razumjeli u zapis koji je nedvojbeno pogrešan. To načelo je isti ugovor primijenjen na VBA projekte i njihove vanjske reference kroz ciklus učitaj-i-spremi, i to je razlika između biblioteke koja stvarne datoteke vraća netaknutima i one koja vraća datoteke koje njezin testni paket slučajno sadrži. Radna knjiga koja je prošla kroz petnaest godina verzija Excela, generator izvještaja i dva alata za migraciju sadržavat će zapise koje nitko živući danas nije dizajnirao. Zapišite ih natrag onakve kakve ste ih našli

Tipizirana klasifikacija SupBook i XTI zapisa stigla je u HotXLS 2.361.2 do 2.361.4, zajedno s ograničenim razrješavanjem XTI-a i sigurnijim putem ConvertFormulasToValues opisanim ovdje. Ako održavate Delphi ili C++Builder kod koji čita naslijeđene xls datoteke s add-in pozivima, DDE ili OLE vezama ili vanjskim definiranim imenima, HotXLS Delphi spreadsheet komponenta rukuje cijelom taksonomijom izvorno, bez instalacije Excela i bez OLE automatizacije na stroju koji radi posao