Техническа статия

Класификация на BIFF SupBook и XTI външни връзки в Delphi

Отворете стар xls, запазете го отново и add-in формулата, която извикваше регистрирана анализна библиотека, вече сочи към празна препратка вътре в самата работна книга. HotXLS проследява това мълчаливо повреждане до едно лошо допускане: че един BIFF SupBook запис е или self, или външен файл. [MS-XLS] дефинира седем вида, а не два

Защо запазена работна книга губи add-in връзките си?

Защото класификационният тест беше структурен, а не типизиран. Традиционният shortcut чете SupBook запис ($01AE), проверява дали носи self маркера, и ако не, третира какъвто и да е низ след него като URL на документ. Всеки запис, който не е нито едното от двете, пропада в клон по подразбиране, а клонът по подразбиране почти винаги е „това е самата работна книга“. Add-in поддържаща връзка, same-sheet връзка, неизползван слот и отрязан запис приключват всички с един и същ грешен етикет. Нищо не хвърля изключение, докато това се случва: записът се парсва, формулата се рекомпилира, файлът се запазва без предупреждение, а дефектът излиза наяве три седмици по-късно, когато някой забележи колона от нули там, където някога е имало валутно преобразуване. [MS-XLS] §2.4.271 описва запис, който може да е self-препратка, same-sheet препратка, контейнер за add-in функции, външна работна книга с виртуален път и таблица с имена на листове, DDE или OLE връзка за данни, или неизползван placeholder — и седмо състояние, което не е в спецификацията, но съществува на реални дискове: записът, който не се парсва. Поправката не е по-добра евристика; тя е отказ да има евристика изобщо

Седемте вида, които носи един SupBook запис

HotXLS декларира таксономията на поддържащите връзки като затворено изброяване в lxExternSheet.pas, и всяко решение надолу по веригата се разклонява по него. Девет стойности на изброяването покриват седемте категории, защото случаят DDE и OLE се нуждае от временно състояние, преди да може да бъде разрешен:

type
  TXLSSupportingLinkKind = (
    slkUnknown,           // неуспешен парсинг, или са останали крайни байтове
    slkSelf,              // тази работна книга
    slkSameSheet,         // U+0000 маркер
    slkAddIn,             // контейнер за add-in функции
    slkExternalWorkbook,  // виртуален път + таблица с имена на листове
    slkDde,               // разрешен от флаговете на ExternName
    slkOle,               // разрешен от флаговете на ExternName
    slkDdeOrOle,          // едно от двете, още не е ясно кое
    slkUnused);           // placeholder с един интервал

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

  TXLSXtiInfo = record
    XtiIndex    : Integer;   // базиран на нула, както се пази в ExternSheet.rgXTI
    ExternID    : Integer;   // базиран на едно, вътрешната конвенция
    SupBookIndex: Integer;
    Sheet1Index : Integer;
    Sheet2Index : Integer;
    LinkKind    : TXLSSupportingLinkKind;
  end;

Разпределението се води по сентинели, не по низове. Стойност на полето $0401 маркира self записа. Брой листове едно, сдвоен с $3A01, маркира add-in контейнер. Само стойност в диапазона 1 до $00FF означава, че следва кодиран виртуален път, и едва тогава HotXLS изобщо декодира низ. Всичко извън тези три форми остава slkUnknown, а запис, чиято таблица с имена на листове не изразходва точно тялото на записа, е връщан обратно в slkUnknown дори когато заглавната част е изглеждала правдоподобно

Стълбата от сентинели, с която HotXLS класифицира BIFF SupBook запис в седем вида, декодирайки низ само за стойности в диапазона на кодираните пътища и отстъпвайки към неизвестен вид вместо към клон по подразбиране
Всеки вид се достига по сентинел, а не по низов тест, и запис, който не съответства на нито една форма, остава неизвестен вместо да пропада в клон по подразбиране, който значи тази работна книга

Защо same-sheet маркерът се декодира като празен низ?

Защото универсалният BIFF четец на низове унищожава байта, от който зависи класификацията. Поддържащата връзка same-sheet е низ от един символ, чийто единствен символ е U+0000, и TXLSBlob.GetBiffString я връща като празен WideString, неразличим от наистина празен път — което е точно входът, на който една self-евристика отговаря „self“. HotXLS затова чете суровата първа кодова точка от тялото на записа, вместо да се доверява на декодираната стойност:

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)     // компресиран, един байт
  else
    FirstChar := Data.GetWord(StringOffset + 3);    // широк, два байта
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;

Обърнете внимание на разклонението компресиран срещу широк. Опцийният байт стои на фиксирано отместване от заглавката на низа, а първата кодова точка е един или два байта според бит 0, така че безусловното ѝ четене като байт работи на повечето файлове и се проваля на тези, писани от локализирани версии — най-лошото възможно разпределение за бъг. Неизползваният placeholder се хваща по същия начин, по буквалния си payload от един интервал, а случаят DDE или OLE — по U+0003 разделителя, вграден в кодирания път

Защо HotXLS чете суровата първа кодова точка от тялото на BIFF SupBook запис вместо декодирания низ, защото универсалният четец на низове превръща same-sheet U+0000 маркера в празна стойност
Same-sheet маркерът е низ от един символ, чийто символ е U+0000, така че универсалният четец го сгъва в празна стойност и само суровата кодова точка на отместването на опцийния байт го запазва

Защо DDE и OLE не могат да се разделят при SupBook?

Защото SupBook записът не носи разграничителните битове. Той ви казва, че връзката е едно от двете; флаговете fOle и fOleLink, които решават кое, живеят в ExternName записа ($0023), който пристига по-късно в потока. HotXLS записва slkDdeOrOle по време на парсинга и го стеснява в ParseExternalName, а ако никакъв ExternName не пристигне, видът остава временен завинаги — което е коректно, защото файлът наистина не го казва. Всеки потребител надолу по веригата третира тази временна стойност като реална, а не като липсваща, така че никой извикващ не трябва да измисля тайбрек. Гадането на „вероятно DDE“ тук би купило по-подредено изброяване и клас грешни отговори, които никой не би могъл да проследи до източника:

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 индексите са базирани на нула на диска и на едно вътре

HotXLS извършва преобразуването с едно отместване точно веднъж — в момента, в който токен навлиза във вътрешното синтактично дърво, и никъде другаде. PtgNameX.ixti ([MS-XLS] §2.5.198.85) е базиран на нула индекс в масива rgXTI на ExternSheet записа ($0017, §2.4.106), докато вътрешната конвенция на библиотеката за ExternID е базирана на едно, като нула е запазена за „без външен лист“. Четящият път на BIFF8 прави FExternID := wValue + 1, когато декодира tNameX токен, а пишещият път излъчва StoreExternID - 1, оставяйки суровия токен изглед и семантиката на диска непокътнати. Грешката тук е необичайно трудна за улавяне: външните дефинирани имена се разрешават до съседния запис, а във файл с един-единствен XTI запис индекс 0 става индекс 1, не намира нищо и името се деградира мълчаливо. Регресия, която упражнява само рекомпилиран формул текст, никога не го вижда, защото рекомпилацията изобщо не докосва дисковия индекс — същият капан, който прави дефинираните имена, обхващащи листове и работни книги, достойни за тестване срещу реални байтови потоци. Разрешаването е оградено от двете страни: TlxExternSheetSheet.TryResolveXti връща False за отрицателен индекс или липсващ запис, TXLSSupBook.TryGetKind връща False за SupBook индекс извън масива, а ClassifyXti след това картографира slkSelf и slkSameSheet към frcInternal, slkExternalWorkbook към frcExternalWorkbook, а slkAddIn, slkDde, slkOle и slkDdeOrOle към frcExternalOther. Всичко останало, включително всеки извъндиапазонен път, попада на frcUnknownOrMalformed

HotXLS преобразува базирания на нула XTI индекс на BIFF PtgNameX токен в базирания на едно вътрешен ExternID в един-единствен момент, с оградено разрешаване от двете страни и картата на класификацията, която го потребява
Отместването с едно между базирания на нула дисков индекс и базирания на едно вътрешен ExternID се прилага веднъж, когато токен навлиза в синтактичното дърво, а всеки неразрешим индекс попада в класа на повредените

Класифициране на формула преди замразяване

TXLSCompiledFormula.ClassifyReferences сканира директно запазения BIFF токен поток, вместо да декомпилира формулата и да търси квадратни скоби. Ловът на скоби в формул текст е текстова евристика с палто на парсер: хваща низови литерали, хваща структурирани препратки и изобщо пропуска външните дефинирани имена, защото в декомпилиран вид те не носят скоби. Токен сканирането гледа само PtgNameX, PtgRef3d, PtgArea3d, PtgRefErr3d и PtgAreaErr3d, отстъпвайки към обход на синтактичното дърво, когато никакъв BIFF поток не е оцелял. Сливането е умишлено песимистично — фиксираната приоритетност е frcUnknownOrMalformed, после frcExternalWorkbook, после frcExternalOther, после frcInternal — така че един-единствен нечетим токен отравя цялата формула. За външно дефинирано име се валидира и индексът на името: базиран на едно, в диапазона и подкрепен от задържан ExternName запис

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 е базиран на едно
    begin
      Sheet := Wb.Sheets[i];
      // замразява САМО формули, класифицирани frcExternalWorkbook;
      // вътрешни, add-in, DDE/OLE и повредени препратки остават формули
      Sheet.ConvertFormulasToValues(True);
    end;
    Wb.SaveAs('quarterly-detached.xls');
  finally
    Wb.Free;
  end;
end;

Параметърът OnlyExternal е мястото, където таксономията се изплаща. Замразяването на формула е необратимо, така че операцията трябва да докаже, че една препратка е външна работна книга, а не просто да я подозира. Add-in извикванията оцеляват, DDE и OLE връзките оцеляват и всичко, което парсерът не е могъл да разбере напълно, оцелява, защото безопасният изход от несигурността е да не се променя нищо. Същата дисциплина управлява преповръзването на формули, копирани между работни книги, където погрешно класифицирана препратка се преповръзва към грешната книга вместо да се провали шумно

Записите, които не се парсват, се записват обратно недокоснати

HotXLS запазва оригиналния SupBook payload и го излъчва отново байт по байт, когато записът не е бил редактиран. Провал в парсването задава slkUnknown и изчиства производното състояние, но уловеното тяло остава в FRawData и пътят на записа го предпочита пред всяка реконструкция, докато елементът не е мръсен и не е self записът. Алтернативата — нормализиране на непарсиран запис в self-препратка, за да има пишещият какво добре формирано да излъчи — превръща запис, който не сте разбрали, в запис, който е категорично грешен. Този принцип е същият договор, приложен към VBA проектите и техните външни препратки през цикъл зареждане-запазване, и той е разликата между библиотека, която обикаля реални файлове, и такава, която обикаля файловете, които тестовият ѝ пакет случайно съдържа. Работна книга, минала през петнайсет години Excel версии, генератор на отчети и два инструмента за миграция, ще съдържа записи, които никой жив не е проектирал. Запишете ги обратно така, както сте ги заварили

Типизираната класификация на SupBook и XTI записи пристигна в HotXLS 2.361.2 до 2.361.4, заедно с ограденото XTI разрешаване и по-безопасния ConvertFormulasToValues път, описан тук. Ако поддържате Delphi или C++Builder код, който чете наследени xls файлове с add-in извиквания, DDE или OLE връзки, или външни дефинирани имена, HotXLS Delphi компонентът за електронни таблици обработва цялата таксономия нативно, без инсталиран Excel и без OLE автоматизация на машината, която върши работата