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
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 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
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