Mở một tệp xls cũ, lưu lại lần nữa, và công thức add-in từng gọi vào một thư viện phân tích đã đăng ký giờ lại trỏ tới một tham chiếu rỗng ngay bên trong chính workbook. HotXLS truy nguyên sự hỏng âm thầm đó về một giả định sai: rằng bản ghi BIFF SupBook hoặc là self hoặc là tệp ngoài. [MS-XLS] định nghĩa bảy loại, chứ không phải hai
Vì sao workbook lưu lại lại mất liên kết add-in?
Vì phép kiểm tra phân loại mang tính cấu trúc thay vì theo kiểu. Lối tắt truyền thống đọc một bản ghi SupBook ($01AE), xem nó có mang marker self hay không, và nếu không thì coi chuỗi theo sau là một URL tài liệu. Mọi bản ghi không thuộc hai trường hợp đó đều rơi xuống nhánh mặc định, và nhánh mặc định gần như luôn là "đây chính là workbook này". Một liên kết hỗ trợ add-in, một liên kết same-sheet, một slot không dùng và một bản ghi bị cụt đều đeo cùng một nhãn sai. Không có gì văng ra trong lúc đó: bản ghi đã parse, công thức đã biên dịch lại, tệp đã lưu mà không có cảnh báo, và lỗi chỉ lộ diện ba tuần sau khi ai đó nhận ra một cột toàn số 0 ở chỗ vốn là phép đổi tiền tệ. [MS-XLS] §2.4.271 mô tả một bản ghi có thể là tham chiếu self, tham chiếu same-sheet, container hàm add-in, workbook ngoài với đường dẫn ảo và bảng tên sheet, liên kết dữ liệu DDE hoặc OLE, hoặc một placeholder không dùng — cộng thêm trạng thái thứ bảy không có trong đặc tả nhưng tồn tại trên đĩa thật: bản ghi không parse nổi. Cách sửa không phải một heuristic tốt hơn; là từ chối hẳn mọi heuristic
Bảy loại mà một bản ghi SupBook có thể mang
HotXLS khai báo hệ phân loại supporting-link thành một enumeration đóng trong lxExternSheet.pas, và mọi quyết định phía sau đều switch trên nó. Chín giá trị enumeration phủ bảy nhóm, vì trường hợp DDE và OLE cần một trạng thái tạm trước khi phân giải được:
type
TXLSSupportingLinkKind = (
slkUnknown, // parse thất bại, hoặc còn byte thừa
slkSelf, // chính workbook này
slkSameSheet, // marker U+0000
slkAddIn, // container hàm add-in
slkExternalWorkbook, // đường dẫn ảo + bảng tên sheet
slkDde, // phân giải từ cờ ExternName
slkOle, // phân giải từ cờ ExternName
slkDdeOrOle, // một trong hai, chưa biết cái nào
slkUnused); // placeholder một dấu cách
TXLSFormulaReferenceClass = (
frcInternal,
frcExternalWorkbook,
frcExternalOther,
frcUnknownOrMalformed);
TXLSXtiInfo = record
XtiIndex : Integer; // zero-based, như lưu trong ExternSheet.rgXTI
ExternID : Integer; // one-based, quy ước nội bộ
SupBookIndex: Integer;
Sheet1Index : Integer;
Sheet2Index : Integer;
LinkKind : TXLSSupportingLinkKind;
end;
Việc điều phối dựa trên sentinel chứ không dựa trên chuỗi. Giá trị trường $0401 đánh dấu bản ghi self. Sheet count bằng một đi kèm $3A01 đánh dấu container add-in. Chỉ giá trị nằm trong khoảng 1 đến $00FF mới nghĩa là theo sau là một đường dẫn ảo đã mã hóa, và chỉ lúc đó HotXLS mới decode một chuỗi. Bất cứ thứ gì ngoài ba hình dạng đó đều giữ nguyên slkUnknown, và một bản ghi mà bảng tên sheet không tiêu thụ đúng phần thân của nó sẽ bị hạ ngược về slkUnknown kể cả khi phần đầu trông hợp lệ
Vì sao marker same-sheet decode thành một chuỗi rỗng?
Vì bộ đọc chuỗi BIFF đa năng phá hủy chính byte mà phép phân loại phụ thuộc vào. Liên kết supporting same-sheet là một chuỗi một ký tự mà ký tự duy nhất đó là U+0000, và TXLSBlob.GetBiffString trả nó về như một WideString rỗng, không thể phân biệt với một đường dẫn rỗng thật — chính là input mà một heuristic tham chiếu self trả lời "self". Vì vậy HotXLS đọc thẳng code point đầu tiên từ phần thân bản ghi thay vì tin vào giá trị đã 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) // nén, một byte
else
FirstChar := Data.GetWord(StringOffset + 3); // wide, hai byte
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;
Chú ý nhánh nén-so-với-wide. Byte tùy chọn nằm ở offset cố định tính từ header chuỗi và code point đầu tiên dài một hay hai byte tùy bit 0, nên đọc nó vô điều kiện như một byte chạy đúng trên đa số tệp và hỏng trên các tệp do bản build bản địa hóa ghi ra — phân bố tệ nhất có thể cho một bug. Placeholder không dùng bị bắt theo cùng cách, qua payload literal một dấu cách, còn trường hợp DDE hay OLE qua dấu phân tách U+0003 nhúng trong đường dẫn đã mã hóa
Vì sao DDE và OLE không thể tách ngay lúc đọc SupBook?
Vì bản ghi SupBook không mang các bit phân biệt. Nó chỉ nói rằng liên kết thuộc một trong hai; các cờ fOle và fOleLink quyết định là cái nào nằm trong bản ghi ExternName ($0023) tới sau trong stream. HotXLS ghi nhận slkDdeOrOle lúc parse và thu hẹp nó trong ParseExternalName, và nếu không có ExternName nào tới thì loại này giữ trạng thái tạm mãi — điều đó đúng, vì tệp thật sự không nói. Mọi consumer phía sau coi giá trị tạm đó là một giá trị thật chứ không phải một giá trị thiếu, nên không caller nào phải tự nghĩ ra cách phân xử. Đoán "chắc là DDE" ở đây đổi lấy một enumeration gọn hơn và một lớp câu trả lời sai mà không ai truy được về nguồn:
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;
Chỉ số XTI zero-based trên đĩa và one-based bên trong
HotXLS thực hiện phép chuyển off-by-one đúng một lần, tại điểm một token đi vào cây cú pháp nội bộ, và không ở đâu khác. PtgNameX.ixti ([MS-XLS] §2.5.198.85) là chỉ số zero-based vào mảng rgXTI của bản ghi ExternSheet ($0017, §2.4.106), trong khi quy ước ExternID nội bộ của thư viện là one-based với số 0 dành cho "không có sheet ngoài". Đường đọc BIFF8 làm FExternID := wValue + 1 khi decode token tNameX và đường ghi phát StoreExternID - 1, giữ nguyên khung nhìn token thô và ngữ nghĩa trên đĩa. Lỡ cái này thì bắt lỗi cực khó: defined name ngoài phân giải sang mục kề bên, và trong một tệp chỉ có một mục XTI, chỉ số 0 thành 1, trượt, và cái tên suy tàn âm thầm. Một regression chỉ chạy lại văn bản công thức đã biên dịch sẽ không bao giờ thấy nó, vì việc biên dịch lại không đụng tới chỉ số trên đĩa — chính cái bẫy khiến defined names phủ qua sheet và workbook đáng được kiểm thử với byte stream thật. Phân giải bị chặn ở cả hai đầu: TlxExternSheetSheet.TryResolveXti trả False cho chỉ số âm hoặc mục thiếu, TXLSSupBook.TryGetKind trả False cho chỉ số SupBook ngoài mảng, và ClassifyXti sau đó ánh xạ slkSelf và slkSameSheet sang frcInternal, slkExternalWorkbook sang frcExternalWorkbook, còn slkAddIn, slkDde, slkOle và slkDdeOrOle sang frcExternalOther. Mọi thứ còn lại, kể cả mọi đường ngoài phạm vi, đều rơi vào frcUnknownOrMalformed
Phân loại công thức trước khi đóng băng nó
TXLSCompiledFormula.ClassifyReferences quét thẳng stream token BIFF được giữ lại thay vì decompile công thức rồi săn dấu ngoặc vuông. Săn ngoặc trong văn bản công thức là một heuristic văn bản mặc áo parser: nó khớp string literal, khớp structured reference, và bỏ sót hoàn toàn defined name ngoài, vì những cái đó không mang ngoặc ở dạng decompiled. Phép quét token chỉ nhìn PtgNameX, PtgRef3d, PtgArea3d, PtgRefErr3d và PtgAreaErr3d, quay về walk cây cú pháp khi không còn stream BIFF nào. Việc gộp được cố ý bi quan — độ ưu tiên cố định là frcUnknownOrMalformed, rồi frcExternalWorkbook, rồi frcExternalOther, rồi frcInternal — nên chỉ một token đọc không nổi cũng đầu độc cả công thức. Với defined name ngoài, chỉ số tên cũng được kiểm tra: one-based, trong phạm vi, và có bản ghi ExternName được giữ lại chống lưng
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 là one-based
begin
Sheet := Wb.Sheets[i];
// chỉ đóng băng công thức được phân loại frcExternalWorkbook;
// tham chiếu nội bộ, add-in, DDE/OLE và malformed giữ nguyên là công thức
Sheet.ConvertFormulasToValues(True);
end;
Wb.SaveAs('quarterly-detached.xls');
finally
Wb.Free;
end;
end;
Tham số OnlyExternal là nơi hệ phân loại trả lại giá trị của nó. Đóng băng một công thức là không thể đảo ngược, nên thao tác này phải chứng minh được một tham chiếu là workbook ngoài chứ không chỉ nghi ngờ. Lời gọi add-in sống sót, liên kết DDE và OLE sống sót, và bất cứ thứ gì parser chưa hiểu trọn vẹn đều sống sót, vì kết quả an toàn của sự mơ hồ là không đổi gì cả. Kỷ luật tương tự chi phối rebinding công thức copy giữa các workbook, nơi một tham chiếu phân loại sai sẽ rebind sang nhầm cuốn thay vì hỏng ầm ĩ
Bản ghi không parse nổi được ghi trả nguyên vẹn
HotXLS giữ payload SupBook gốc và phát lại nó từng byte khi bản ghi chưa từng bị sửa. Một lần parse thất bại đặt slkUnknown và xóa trạng thái suy ra, nhưng phần thân đã bắt giữ vẫn nằm trong FRawData và đường lưu ưu tiên nó hơn mọi bản dựng lại miễn là item không dirty và không phải bản ghi self. Phương án thay thế — chuẩn hóa một bản ghi không parse nổi thành tham chiếu self để writer có thứ gì đó well-formed để phát — biến một bản ghi bạn không hiểu thành một bản ghi chắc chắn sai. Nguyên tắc đó chính là cùng một hợp đồng áp cho VBA project và các tham chiếu ngoài của nó qua một vòng load-and-save, và đó là ranh giới giữa một thư viện round-trip được tệp đời thực và một thư viện chỉ round-trip được những tệp mà bộ test của nó tình cờ chứa. Một workbook đi qua mười lăm năm các phiên bản Excel, một trình sinh báo cáo và hai công cụ di trú sẽ chứa những bản ghi mà không ai còn sống ngày nay từng thiết kế. Hãy ghi trả chúng như bạn tìm thấy
Phân loại SupBook và XTI theo kiểu có cấu trúc phát hành trong HotXLS 2.361.2 tới 2.361.4, cùng phân giải XTI có chặn và đường ConvertFormulasToValues an toàn hơn được mô tả ở đây. Nếu bạn duy trì code Delphi hoặc C++Builder đọc tệp xls legacy chứa lời gọi add-in, liên kết DDE hoặc OLE, hay defined name ngoài, HotXLS Delphi spreadsheet component xử trọn hệ phân loại này bản địa, không cần cài Excel và không OLE automation trên máy làm việc