Excel 活頁簿可以攜帶 EMF 與 WMF 圖片,而傳統的畫法是把位元組串流交給作業系統的中繼檔播放器。這個決策值得直視:中繼檔是給圖形 API 用的序列化命令串流,重播一份中繼檔,等於讓一份透過電子郵件抵達的檔案驅動圖形驅動程式。HotXLS 走另一條路。XLSDecodeVectorScene 自己解析中繼檔,驗證標頭、每個記錄大小、宣告的記錄總數與檔案結尾記錄的精確位置,斷然拒絕 escape 記錄,回傳一個由基本繪圖命令組成的 TXLSVectorScene,Canvas 與 SVG 後端用自己的程式碼重播它。任何環節都不涉及驅動程式播放
這是一筆以涵蓋換圍堵的交易。面向矩形的命令白名單無法重現設計師能造出的每份中繼檔,所以場景會回報它無法表現的繪圖記錄數量,由呼叫端決定怎麼處理。對一個渲染不是自己產出文件的伺服器行程來說,這筆交易的方向是對的
為什麼中繼檔播放不適合不受信任的輸入?
因為這個格式不是圖片,是程式。EMF 記錄串流操縱裝置內容狀態堆疊,從 handle 表配置並選取物件,還能攜帶其負載被直通裝置驅動程式的 escape 記錄。重播它,會演練平台圖形堆疊中那些以「中繼檔來自同一台機器上合作的應用程式」為前提撰寫的路徑。當輸入是試算表附件時,這個前提不存在,而試算表程式庫內部再小心也沒用,因為做解析的元件不是它
這與治理容器層的推理相同。活頁簿是一個 ZIP 壓縮檔,HotXLS 驗證它的中央目錄而不是信任宣告的偏移,如ZIP 中央目錄結尾驗證文章所述。中繼檔負載是同一問題的下一層
解碼器在畫任何東西之前檢查什麼
驗證是結構性的,而且事先完成,因為一個邊畫邊驗證的剖析器,已經對未驗證的資料採取了行動。標頭必須嚴格相符而不是貌似相符。每個記錄必須宣告一個放得進剩餘緩衝區、又大到足以容納自身固定欄位的大小。標頭宣告的記錄數必須與實際在場的記錄相符。檔案結尾記錄必須正好落在串流結束處,而不是只在附近,這封死了把第二份負載藏在有效圖片後面的尾部垃圾技倆
在結構之外,解碼器在語意上 fail-closed。escape 記錄被拒絕,不是被跳過。解碼器未建模的改變狀態記錄會讓解碼失敗,而不是被忽略,因為忽略一個狀態改變,意味著後續每個繪圖命令都在一個檔案沒有要求的狀態下執行,結果是一幅以無人能預測的方式出錯的圖。支援命令集之外的繪圖記錄則是另一回事:那些被計數並跳過,因為缺一個形狀是可見、可回報的缺口,而不是無聲的損壞
預算是格式契約的一部分
向量格式有自己版本的解壓縮炸彈。幾 KB 的記錄可以宣告數億個點的折線,或宣告尺寸相乘後以 TB 計的影像。因此界限必須是明確的常數,而不是這台機器碰巧撐得過的值
// 來自 lxVectorScene:解碼預算,明文陳述而非隱含
XL_VECTOR_MAX_RECORDS = 1000000;
XL_VECTOR_MAX_HANDLES = 4096;
XL_VECTOR_MAX_DC_DEPTH = 32;
XL_VECTOR_MAX_COMMANDS = 100000;
XL_VECTOR_MAX_POINTS_PER_RECORD = 100000;
XL_VECTOR_MAX_TOTAL_POINTS = 2000000;
XL_VECTOR_MAX_TEXT_CHARS = 4096;
XL_VECTOR_MAX_TOTAL_TEXT_CHARS = 1000000;
XL_VECTOR_MAX_IMAGE_SIDE = 8192;
XL_VECTOR_MAX_IMAGE_PIXELS = 32 * 1024 * 1024;
XL_VECTOR_MAX_IMAGE_BYTES = 64 * 1024 * 1024;
XL_VECTOR_MAX_COORD = 1000000000;
其中兩個值得註記。裝置內容深度上限 32 的存在,是因為 SaveDC 與 RestoreDC 記錄會巢狀,不平衡的串流可以無限推入;對真實中繼檔來說 32 已經寬裕,執行起來也便宜。座標上限的存在,是因為座標會餵進轉換,接近整數範圍極限的值會產出無限大或回繞的轉換結果,之後下游每個包圍盒計算都是胡話。在剖析時鉗制座標,比在幾何的每個消費端設防容易推理得多
使用場景
解碼器交回一個您擁有的物件、一個命令數、一個名義尺寸,以及它選擇不表現的繪圖記錄數
uses
lxVectorScene;
var
Scene: TXLSVectorScene;
Error: WideString;
I: Integer;
begin
// Data 持有從活頁簿取出的原始圖片負載
if not XLSDecodeVectorScene(Data, xlsvfEmf, Scene, Error) then
begin
// 被拒:標頭、界限、總數、EOF 位置或預算
LogReject('metafile rejected: ' + Error);
Exit;
end;
try
if Scene.SkippedDrawRecords > 0 then
LogWarning(Format('%d drawing records outside the safe subset',
[Scene.SkippedDrawRecords]));
for I := 0 to Scene.Count - 1 do
case Scene.Commands[I].Kind of
xlsvcRectangle: DrawRect(Scene.Commands[I]);
xlsvcEllipse: DrawEllipse(Scene.Commands[I]);
xlsvcPolyline,
xlsvcPolygon,
xlsvcBezier: DrawPath(Scene.Commands[I]);
xlsvcText: DrawText(Scene.Commands[I]);
xlsvcImage: DrawImage(Scene.Commands[I]);
end;
finally
Scene.Free;
end;
end;
命令記錄攜帶後端需要的一切,不帶任何需要裝置的東西:畫筆的存在、色彩、寬度與樣式;筆刷的存在與色彩;幾何;文字則有字串、字型名稱、大小、樣式與對齊。這正是同一個場景能同時供螢幕畫布渲染器與 SVG 寫出器使用的原因,也是向量路徑在預覽與匯出之間不分岔的原因。工作表內容的螢幕渲染一般性描述於自訂 VCL 網格渲染文章
拒絕一張圖片不會傷害活頁簿
這個設計的一個重要性質是:被拒的解碼只影響渲染。原始負載留在模型裡,所以一份被打開再另存的活頁簿,無論安全解碼器畫不畫得出來,都逐位元組帶出它的中繼檔圖片。既有的有界限點陣路徑也仍可作為回退。換句話說,嚴格剖析器把關的是什麼被執行,而不是什麼被保留,這個區別讓一個出於安全動機的改動出貨時,不會變成造成資料損失的改動
繪圖物件處理的一般情況,包括物件模型中在來回轉換後原樣保留的部分,在圖表、影像與繪圖文章中涵蓋
這讓伺服器部署處於什麼位置
如果您在服務裡渲染使用者上傳的活頁簿,現在的實務立場站得住腳:中繼檔圖片由您可以稽核的程式碼解析,由您讀得懂的常數設界,永遠不交給圖形驅動程式。誠實的警告是涵蓋。繪圖工具產出的複雜中繼檔會撞上跳過記錄計數器,對此的正確回應是把計數器浮上來,而不是悄悄擴大白名單。一幅部分渲染且明說的圖片,是一場支援對話;一幅渲染錯誤卻不吭聲的圖片,是一張來自客戶的錯誤報告
HotXLS 在 Delphi 與 C++Builder 中原生處理 XLS、XLSX、ODS 與 CSV,無需安裝 Excel,同樣的有界限解析哲學貫穿它的容器、公式與繪圖層。格式與安全細節列於 HotXLS Delphi spreadsheet component 產品頁