程式設計面試:如何實作 Unicode 原始碼欺騙診斷器?
題幹與適用場景
程式碼儲存庫允許非 ASCII 識別與多語言註解。有人提交包含雙向控制字元或視覺混淆識別的變更,普通文字檢視器卻顯示成容易誤讀的順序。請設計掃描器,既能發現風險,也不把合法的阿拉伯文、希伯來文註解與字串一律拒絕。
面試官考察點
- 能否把詞法分析、雙向渲染與安全診斷拆成獨立階段。
- 是否理解 UTS #55 以詞法結構定義 source-code atoms,並使用 HL4 語意顯示程式碼。
- 能否使用 UTS #39 的 confusable 與限制規則,而不是自創「非 ASCII 即惡意」判斷。
- 是否能處理 Unicode 資料版本、增量掃描、可解釋報告與誤報治理。
作答前的澄清問題
先確認目標語言、編譯器版本、允許的識別腳本與註解語言;確認診斷器執行在編輯器、CI 還是程式碼託管平台。再確認策略是僅告警、阻擋提交還是人工複核,以及是否需要相容舊 Unicode 資料和無法解析的程式碼片段。
30 秒回答框架
我會先用目標語言的詞法分析器切出 token、識別、字串與註解,再對每個 atom 記錄原始碼點、方向控制與腳本集合。原始碼顯示遵循 UTS #55 的詞法結構,而不是把整個檔案當普通段落。安全診斷結合 UTS #39 的限制等級與 confusable 資料,報告位置、規則、版本與可見渲染差異。預設告警並支援依儲存庫策略阻擋;保留原文,避免把合法 RTL 文字誤判為攻擊。
分步驟深入解答
第一步:先建立詞法邊界
不能逐字套用普通段落的雙向演算法。數字常值、識別、單行字串與註解內容應形成原子;巢狀語言還要依外層語法劃分邊界。掃描器應重用編譯器或成熟 parser 的 token 範圍,無法解析時明確降級為保守診斷。
第二步:標記雙向控制與預設可忽略字元
對每個 atom 記錄 RLO、LRO、PDF、RLI、LRI、FSI、PDI 等控制字元及其作用範圍。報告中顯示轉義後的碼點和原始位置,避免控制字元在終端或 Web 頁面再次改變閱讀順序。不要簡單刪除字元,因為合法文字排版可能需要方向控制。
第三步:依 UTS #55 組織程式碼顯示
UTS #55 建議依詞法結構套用高階協定 HL4:token 順序維持語法順序,字串、識別與註解作為不可拆分的 atom,再在 atom 內依方向屬性顯示。這樣可以同時保持 RTL 文字可讀與程式碼結構可稽核。
第四步:執行識別規範與混淆檢測
對識別先套用語言允許的規範化、大小寫與 UAX #31 設定,再依 UTS #39 做腳本混合與 confusable 診斷。不要把 skeleton 當作識別本身;它是同一 Unicode 資料版本下的中間比較值,適合發現潛在衝突。
第五步:寫出最小掃描管線
下面的虛擬碼只展示階段邊界,真實實作仍需接入目標語言 lexer 和 Unicode 資料檔:
type Atom = { kind: string; start: number; end: number; text: string };
type Finding = { code: string; start: number; end: number; detail: string };
function diagnose(source: string, atoms: Atom[], unicodeVersion: string): Finding[] {
const findings: Finding[] = [];
for (const atom of atoms) {
findings.push(...scanBidiControls(atom));
if (atom.kind === "identifier") {
findings.push(...scanIdentifierConfusables(atom, unicodeVersion));
}
}
return findings;
}scanBidiControls 應保存控制字元的碼點和範圍;scanIdentifierConfusables 應回傳腳本混合、限制等級或與既有識別的衝突。診斷器不應在此階段改寫原始碼。
第六步:處理版本、快取與增量掃描
將 Unicode 資料版本、語言 lexer 版本和規則設定寫入報告。依檔案內容和 lexer 版本快取 token 結果;增量建置只重掃受影響的 token 與同一作用域內的識別。升級 Unicode 資料後重新計算 skeleton 和衝突集合,不能直接重用舊快取。
第七步:設計報告與策略分離
報告需要包含檔案、行列、規則碼、原始碼點、可見渲染和修復建議。策略層再決定警告、阻擋、人工複核或允許;同一診斷結果可以在編輯器、CI 和程式碼託管介面採用不同策略。日誌避免直接渲染未轉義的控制字元。
高品質示範回答
我會把實作拆成 lexer、Unicode 分析、渲染預覽與策略報告四層。lexer 輸出 token、識別、字串和註解的精確範圍;無法解析時標記不確定區間,不假設逐字邊界。Unicode 分析對每個 atom 記錄 Bidi_Control、腳本集合、規範化設定和 UTS #39 confusable 結果。渲染預覽依 UTS #55 的 HL4 思路保持 token 的語法順序,再在 atom 內套用雙向規則,旁邊同時顯示轉義碼點。報告攜帶 Unicode 資料版本、lexer 版本、規則碼和位置,策略層依欄位風險選擇警告或阻擋。識別衝突可以用同版本 skeleton 輔助發現,但 skeleton 不能作為使用者名稱或穩定雜湊。快取鍵包含檔案內容、語言版本和 Unicode 版本,升級時批次重算。測試涵蓋 RTL 註解、字串中的方向控制、巢狀語言、混合腳本識別、無法解析程式碼、增量修改與終端/Web 渲染差異。
常見誤區與失敗方案
- 逐字從左至右顯示,導致合法 RTL 註解和字串不可讀。
- 看到非 ASCII 就阻擋,誤傷合法語言和國際化識別。
- 用正則取代 lexer,無法判斷控制字元位於字串、註解或識別內。
- 只保存清洗後文字,失去稽核所需的原始碼點和位置。
- 直接比較不同 Unicode 版本產生的 skeleton,忽略資料遷移。
延伸追問與參考答案
為什麼不能直接刪除所有 Bidi_Control?
某些合法文字需要方向控制,刪除會改變使用者看到的內容。對原始碼可依語言策略阻擋或轉義,對註解和字串則報告上下文並保留原文。
lexer 失敗時掃描器應該怎麼辦?
回傳明確的不確定範圍,執行保守的控制字元和碼點診斷,並阻止「已安全」結論。CI 可將不確定結果交給人工複核,待語言 parser 支援後再精確掃描。
如何驗證不會誤報多語言程式碼?
準備合法 RTL 註解、字串和允許的識別作為正例,配合 RLO/LRO、腳本混合和 confusable 衝突作為負例;在多個編輯器、終端和 Web 渲染器中比較邏輯順序、可見順序與報告位置。