Artikel Teknis

Klasifikasi Tautan Eksternal SupBook dan XTI BIFF di Delphi

Buka sebuah xls lama, simpan lagi, dan formula add-in yang memanggil pustaka analisis terdaftar kini menunjuk referensi kosong di dalam workbook itu sendiri. HotXLS menelusuri kerusakan senyap itu ke satu asumsi buruk: bahwa record BIFF SupBook itu entah dirinya sendiri atau file eksternal. [MS-XLS] mendefinisikan tujuh jenis, bukan dua

Mengapa workbook yang disimpan kehilangan tautan add-in-nya?

Karena uji klasifikasinya struktural, bukan bertipe. Jalan pintas tradisional membaca record SupBook ($01AE), memeriksa apakah ia membawa penanda self, dan jika tidak, memperlakukan string apa pun yang mengikuti sebagai URL dokumen. Setiap record yang bukan kedua hal itu jatuh ke cabang bawaan, dan cabang bawaan itu hampir selalu "ini workbook itu sendiri". Sebuah tautan pendukung add-in, tautan same-sheet, slot tak terpakai, dan record terpangkas semua berakhir memakai label salah yang sama. Tidak ada yang melempar exception saat ini terjadi: record ter-parse, formula terkompilasi ulang, file tersimpan tanpa peringatan, dan cacatnya muncul tiga minggu kemudian ketika seseorang menyadari kolom nol di tempat konversi mata uang dulu berada. [MS-XLS] §2.4.271 menggambarkan record yang bisa menjadi referensi diri, referensi same-sheet, wadah fungsi add-in, workbook eksternal dengan jalur virtual dan tabel nama sheet, tautan data DDE atau OLE, atau placeholder tak terpakai — dan keadaan ketujuh yang tak tertulis di spesifikasi tapi ada di disk sungguhan, yaitu record yang tidak ter-parse. Solusinya bukan heuristik yang lebih baik; itu menolak memakai heuristik sama sekali

Tujuh jenis yang bisa dibawa record SupBook

HotXLS mendeklarasikan taksonomi tautan pendukung sebagai enumerasi tertutup di lxExternSheet.pas, dan setiap keputusan hilir bercabang di atasnya. Sembilan nilai enumerasi menutup tujuh kategori, karena kasus DDE dan OLE butuh keadaan sementara sebelum bisa diselesaikan:

type
  TXLSSupportingLinkKind = (
    slkUnknown,           // failed to parse, or trailing bytes remained
    slkSelf,              // this workbook
    slkSameSheet,         // U+0000 marker
    slkAddIn,             // add-in function container
    slkExternalWorkbook,  // virtual path + sheet-name table
    slkDde,               // resolved from ExternName flags
    slkOle,               // resolved from ExternName flags
    slkDdeOrOle,          // one of the two, not yet known which
    slkUnused);           // single-space placeholder

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

  TXLSXtiInfo = record
    XtiIndex    : Integer;   // zero-based, as stored in ExternSheet.rgXTI
    ExternID    : Integer;   // one-based, the internal convention
    SupBookIndex: Integer;
    Sheet1Index : Integer;
    Sheet2Index : Integer;
    LinkKind    : TXLSSupportingLinkKind;
  end;

Dispatch-nya digerakkan sentinel, bukan digerakkan string. Nilai field $0401 menandai record self. Jumlah sheet satu berpasangan dengan $3A01 menandai wadah add-in. Hanya nilai di rentang 1 sampai $00FF yang berarti jalur virtual terenkode mengikuti, dan hanya pada saat itu HotXLS men-decode string sama sekali. Apa pun di luar tiga bentuk itu tetap slkUnknown, dan record yang tabel nama sheet-nya tidak menghabiskan body record persis, diturunkan kembali ke slkUnknown bahkan ketika head-nya tampak masuk akal

Tangga digerakkan-sentinel yang dipakai HotXLS untuk mengklasifikasi record BIFF SupBook ke tujuh jenis, men-decode string hanya untuk nilai di rentang jalur terenkode dan jatuh ke jenis tak dikenal alih-alih ke cabang bawaan
Setiap jenis dicapai lewat sentinel alih-alih uji string, dan record yang tak cocok dengan bentuk mana pun tetap tak dikenal alih-alih jatuh ke cabang bawaan yang berarti workbook ini

Mengapa penanda same-sheet ter-decode sebagai string kosong?

Karena pembaca string BIFF serbaguna menghancurkan byte yang diandalkan klasifikasi. Tautan pendukung same-sheet adalah string satu karakter yang karakter tunggalnya U+0000, dan TXLSBlob.GetBiffString mengembalikannya sebagai WideString kosong, tak terbedakan dari jalur yang benar-benar kosong — persis input yang dijawab "self" oleh heuristik referensi-diri. HotXLS karena itu membaca code point mentah pertama dari body record alih-alih mempercayai nilai ter-decode:

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)     // compressed, one byte
  else
    FirstChar := Data.GetWord(StringOffset + 3);    // wide, two bytes
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;

Perhatikan cabang compressed-versus-wide. Byte opsi duduk di offset tetap dari header string dan code point pertama adalah satu byte atau dua tergantung bit 0, jadi membacanya sebagai byte tanpa syarat bekerja di kebanyakan file dan gagal di file yang ditulis build terlokalisasi — distribusi terburuk yang mungkin untuk sebuah bug. Placeholder tak terpakai tertangkap dengan cara yang sama, lewat payload satu-spasi literalnya, dan kasus DDE atau OLE lewat pemisah U+0003 yang tertanam di jalur terenkode

Mengapa HotXLS membaca code point mentah pertama dari body record BIFF SupBook alih-alih string ter-decode, karena pembaca string serbaguna mengubah penanda same-sheet U+0000 menjadi nilai kosong
Penanda same-sheet adalah string satu karakter yang karakternya U+0000, jadi pembaca string umum melipatnya menjadi nilai kosong dan hanya code point mentah di offset byte-opsi yang menjaganya

Mengapa DDE dan OLE tak bisa dipisahkan pada saat SupBook?

Karena record SupBook tidak membawa bit pembedanya. Ia memberi tahu Anda bahwa tautan itu salah satu dari keduanya; flag fOle dan fOleLink yang memutuskan mana berada di record ExternName ($0023) yang tiba belakangan di stream. HotXLS mencatat slkDdeOrOle saat parse dan menyempitkannya di ParseExternalName, dan jika tak ada ExternName yang pernah tiba, jenisnya tetap sementara selamanya — yang memang benar, karena file itu sungguhan tidak mengatakan. Setiap konsumen hilir memperlakukan nilai sementara itu sebagai nilai nyata alih-alih yang hilang, jadi tak ada pemanggil yang perlu mengarang penentu seri. Menebak "kemungkinan besar DDE" di sini akan membeli enumerasi yang lebih rapi dan satu kelas jawaban salah yang tak bisa dilacak siapa pun:

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;

Indeks XTI zero-based di disk dan one-based di dalam

HotXLS melakukan konversi off-by-one itu tepat satu kali, pada titik token masuk pohon sintaks internal, dan tak di tempat lain. PtgNameX.ixti ([MS-XLS] §2.5.198.85) adalah indeks zero-based ke array rgXTI dari record ExternSheet ($0017, §2.4.106), sedangkan konvensi internal ExternID pustaka adalah one-based dengan nol dicadangkan untuk "tanpa sheet eksternal". Jalur baca BIFF8 melakukan FExternID := wValue + 1 saat men-decode token tNameX dan jalur tulis menulis StoreExternID - 1, menyisakan tampilan token mentah dan semantik on-disk tak tersentuh. Salah di sini luar biasa sulit ditangkap: nama defined eksternal ter-resolve ke entri tetangga, dan di file dengan satu entri XTI indeks 0 menjadi indeks 1, meleset, dan nama itu terdegradasi senyap. Regresi yang hanya menguji teks formula terkompilasi ulang tak pernah melihatnya, karena kompilasi ulang tak pernah menyentuh indeks disk — jebakan yang sama yang membuat defined names melintasi sheet dan workbook layak diuji terhadap stream byte nyata. Resolusi berbatas di kedua ujung: TlxExternSheetSheet.TryResolveXti mengembalikan False untuk indeks negatif atau entri yang hilang, TXLSSupBook.TryGetKind mengembalikan False untuk indeks SupBook di luar array, dan ClassifyXti kemudian memetakan slkSelf dan slkSameSheet ke frcInternal, slkExternalWorkbook ke frcExternalWorkbook, serta slkAddIn, slkDde, slkOle, dan slkDdeOrOle ke frcExternalOther. Semua sisanya, termasuk setiap jalur out-of-range, mendarat di frcUnknownOrMalformed

HotXLS mengonversi indeks XTI zero-based token BIFF PtgNameX menjadi ExternID internal one-based-nya di satu titik, dengan resolusi berbatas di kedua ujung dan peta klasifikasi yang mengonsumsinya
Off-by-one antara indeks disk zero-based dan ExternID internal one-based diterapkan sekali, saat token masuk pohon sintaks, dan setiap indeks yang tak teresolve mendarat di kelas malformed

Mengklasifikasi formula sebelum membekukannya

TXLSCompiledFormula.ClassifyReferences memindai stream token BIFF yang dipertahankan secara langsung alih-alih men-decompile formula dan mencari tanda kurung siku. Berburu kurung di teks formula adalah heuristik teks yang memakai jas parser: ia cocok dengan string literal, cocok dengan structured references, dan melewatkan defined name eksternal sepenuhnya, karena yang itu tak membawa kurung dalam bentuk ter-decompile. Pemindaian token hanya melihat PtgNameX, PtgRef3d, PtgArea3d, PtgRefErr3d, dan PtgAreaErr3d, berbalik ke penelusuran pohon sintaks ketika tak ada stream BIFF yang selamat. Penggabungan sengaja pesimis — prioritas tetapnya frcUnknownOrMalformed, lalu frcExternalWorkbook, lalu frcExternalOther, lalu frcInternal — jadi satu token tak terbaca meracuni seluruh formula. Untuk defined name eksternal, indeks namanya divalidasi juga: one-based, dalam rentang, dan ditopang record ExternName yang dipertahankan

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 is one-based
    begin
      Sheet := Wb.Sheets[i];
      // freezes ONLY formulas classified frcExternalWorkbook;
      // internal, add-in, DDE/OLE and malformed references stay formulas
      Sheet.ConvertFormulasToValues(True);
    end;
    Wb.SaveAs('quarterly-detached.xls');
  finally
    Wb.Free;
  end;
end;

Parameter OnlyExternal adalah tempat taksonomi itu membayar dirinya sendiri. Membekukan formula itu tak terpulihkan, jadi operasi ini harus membuktikan referensi adalah workbook eksternal alih-alih sekadar mencurigainya. Panggilan add-in selamat, tautan DDE dan OLE selamat, dan apa pun yang tak sepenuhnya dipahami parser selamat, karena hasil aman dari ketidakpastian adalah tak mengubah apa pun. Disiplin yang sama mengatur rebinding formula yang disalin antar workbook, tempat referensi yang salah klasifikasi ter-rebind ke buku yang salah alih-alih gagal dengan nyaring

Record yang tak ter-parse ditulis kembali tak tersentuh

HotXLS menyimpan payload SupBook asli dan menulisnya ulang byte per byte ketika record itu tak pernah disunting. Kegagalan parse mengatur slkUnknown dan menghapus state turunannya, tetapi body yang terkapur tetap di FRawData dan jalur penyimpanan memilihnya di atas rekonstruksi apa pun selama item tidak kotor dan bukan record self. Alternatifnya — menormalisasi record yang tak ter-parse menjadi referensi-diri agar penulis punya sesuatu yang terbentuk baik untuk ditulis — mengubah record yang Anda pahami menjadi record yang pasti salah. Prinsip itu adalah kontrak yang sama yang diterapkan pada proyek VBA dan referensi eksternalnya melintasi siklus muat-dan-simpan, dan itulah bedanya pustaka yang round-trip file dunia nyata dengan yang round-trip file yang kebetulan ada di test suite-nya. Workbook yang telah melewati lima belas tahun versi Excel, satu generator laporan, dan dua alat migrasi akan memuat record yang tak dirancang siapa pun yang masih hidup. Tulis kembali sebagaimana Anda temukan

Klasifikasi bertipe record SupBook dan XTI dikirim di HotXLS 2.361.2 sampai 2.361.4, bersama resolusi XTI berbatas dan jalur ConvertFormulasToValues yang lebih aman yang dijelaskan di sini. Jika Anda memelihara kode Delphi atau C++Builder yang membaca file xls warisan yang membawa panggilan add-in, tautan DDE atau OLE, atau defined name eksternal, komponen spreadsheet Delphi HotXLS menangani seluruh taksonomi secara native, tanpa instalasi Excel dan tanpa otomatisasi OLE di mesin yang mengerjakannya