| RFC-0033:處理不明欄位和嚴格程度 | |
|---|---|
| 狀態 | 已接受 |
| 區域 |
|
| 說明 | 這項 FTP 修正並釐清 FIDL 解碼器在遇到下列項目時的行為:資料表、可擴充聯集、列舉和位元 (可擴充訊息) 包含類型不明的欄位。 |
| 作者 | |
| 提交日期 (年-月-日) | 2019-02-07 |
| 審查日期 (年-月-日) | 2019-03-07 |
摘要
這項 FTP 修正並釐清 FIDL 解碼器在遇到資料表、可擴充聯集、列舉和位元時的行為,也就是可擴充訊息[^1],其中欄位的類型不明。
具體來說,我們建議:
- 為可擴充訊息定義嚴格和彈性行為,指定解碼器遇到不明欄位 (包括控制代碼) 時應有的行為;
- 可做為可擴充訊息宣告前置字串的
strict關鍵字。這項功能會在驗證期間拒絕郵件,確保收到的郵件不含不明欄位。 - 預設可擴充訊息,以保持彈性,也就是允許不明值,並透過繫結公開這些值;
- 定義並建議繫結提供的 API,供用戶端檢查含有不明欄位的訊息。
與其他 RFC 的關係
這項 RFC 的修訂內容如下:
提振精神
可擴充訊息是相當實用的機制,可讓資料交換格式演進,而不破壞連線格式 (二進位) 相容性。不過,變更結構定義會對 FIDL 解碼器造成設計決策,因為如何驗證、剖析及向使用者公開這些欄位,都會產生疑問。
雖然每種語言的資料結構存取機制和規範不同,但指定解碼器及其 API 的行為可強制執行驗證行為,進而提升安全性,並提高各類型和語言的一致性,改善整體人體工學。
我們也希望為受限環境啟用繫結,在這些環境中,剖析不明欄位可能不是正確運作的必要條件,而且會增加不必要的效能負擔。這也適用於已成熟且預計不會再演進的訊息。
設計
不明欄位是指讀取器無法判斷序數 (表格)、標記 (可擴充聯集)、值 (列舉) 或特定位元 (位元) 的欄位。為簡潔起見,我們將使用「標記」一詞來指稱未知的序數/標記/值/特定位元。
- 含有不明標記的訊息必須通過驗證,並成功剖析。
- 不過,請參閱下文,瞭解嚴格訊息的例外情況。
- 解碼器必須處理訊息中的不明控制代碼。
- 預設的處理行為必須是關閉所有控制代碼。
- 繫結「可能」會提供機制,供用戶特別處理不明控制代碼。
- 繫結「必須」提供機制,偵測是否收到訊息中包含的未知標記。
- 繫結應提供機制,偵測具有指定標記的欄位是否屬於收到的訊息。
- 繫結「可能」會提供機制,用於讀取未知欄位中的標記、原始資料和 (未輸入型別的) 控制代碼。
如果目標語言提供在編譯時詳盡檢查標記的機制 (例如 C/C++ 中的
switch(),Rust 中的match):- 該語言繫結「應」提供特殊的「unknown」標記,可納入詳盡檢查,以便處理所有情況 (例如 C/C++ 中的
default和 Rust 中的_可以省略。 - 這項建議的目的是避免編譯時需要使用 catch-all 案例,因為如果需要,日後新增的標記就不會引發編譯器警告。
- 由於各語言的實作策略可能不同,因此這項 FTP 並未定義應如何達成此目標的機制。
範例:
// Bindings SHOULD NOT offer this API: switch(union.Which()) { case Tag1: ... case Tag2: ... case Tag3: ... default: ... // no unknown tag in bindings forces handling using default case } // Bindings SHOULD offer this API: switch(union.Which()) { case Tag1: ... case Tag2: ... case Tag3: ... case Tag_Unknown: ... // no default case: new tags cause a non-exhaustiveness warning }
- 該語言繫結「應」提供特殊的「unknown」標記,可納入詳盡檢查,以便處理所有情況 (例如 C/C++ 中的
嚴格處理訊息
- 我們導入
strict關鍵字,可做為可擴充訊息宣告的前置字串,例如strict table T { ... }或strict enum T { ... }。 - 含有不明欄位的嚴格訊息必須視為無效。
- 如果繫結支援彈性訊息的這類機制,則不得為嚴格訊息的詳盡標記檢查提供特殊的「未知」標記。
- 從嚴格訊息轉換為彈性訊息,反之亦然,都必須支援做為非破壞性來源層級 (API) 變更,可能使用
[Transitional]屬性進行軟轉換。- 這類轉換「不得」變更連線格式 (ABI)。
嚴格訊息「不」具遞移性。 如果訊息標示為嚴格,則只有該訊息為嚴格。 該訊息內含的子訊息則不嚴格。
語法範例:
// One simply doesn't walk into Mordor and add a new file mode, so this is // reasonable to be strict. strict bits UnixFilePermission : uint16 { ... }; // It's too dangerous for clients to ignore data in this table if we // extend it later, but we wish to keep the wire format compatible if we // do change it, so it's not a struct. strict table SecurityPolicy { ... };
導入策略
- 更新 FIDL 相容性測試,驗證現有語言繫結是否符合這項規格。
- 針對 (1) 只有已知欄位、(2) 只有不明欄位,以及 (3) 至少一個已知和一個不明欄位的訊息,新增測試案例。
- 確認 FIDL 相容性測試已針對所有適當類型的空白訊息,提供測試案例。
- 在
fidlc中新增嚴格訊息支援。 - 更新語言繫結,支援嚴格訊息。
- 在 FIDL 相容性測試中,為嚴格訊息新增測試案例。
預做準備:使用網站修飾符
在設計階段,我們也考慮允許將嚴格關鍵字放在宣告的使用網站中,而不是建議的宣告網站位置。
語法範例:
protocol Important {
SomeMethod(...) -> (strict other.library.Message response);
}
在此,other.library.Message 可能未定義 strict,但我們想使用它,同時要求嚴格驗證。
這會增加繫結作者的設計複雜度,因為嚴格模式和彈性模式可能都需要 other.library.Message。
在編碼/驗證/解碼方面,根據內容為同一則訊息公開嚴格和彈性模式,與處理字串或向量的方式類似。兩者版面配置相同,但可根據使用位置設定不同界線。這也與可擴充聯集在可為空值或不可為空值環境中的使用方式類似。一般來說,繫結會選擇類型結構定義,並以某種方式指出界限、可為空值或嚴格模式 (如本文所述)。
如果針對同一則訊息同時公開嚴格和彈性模式,第二個問題是處理訊息的組裝作業,以及在使用者程式碼中查詢訊息。
舉例來說,假設列舉有三個成員:A、B 和 C。如要公開彈性模式,我們需要特殊的列舉成員「unknown」。因此,現在可以組裝未通過嚴格驗證的列舉,這樣在需要這個列舉的其他嚴格環境中,編碼時就會失敗。同樣地,字串和向量的平行關係也很重要:如果沒有高度專業的 API,繫結會允許建立過長的字串和向量,然後無法編碼。
如果需要同時支援嚴格和彈性模式,請為彈性模式產生所有額外片段,並確保在編碼、解碼和驗證期間,視需要套用嚴格驗證。
人體工學
這項 FTP 改善項目可透過下列幾種方式提升人體工學:
- 我們能更清楚地向使用者說明 FIDL 在各種語言中的行為。
- 嚴格訊息可讓使用者避免編寫不必要的程式碼來處理不明欄位。
說明文件和範例
- 嚴格欄位的文法和語言規格需要更新。
- FIDL 樣式指南應更新,提供何時將訊息宣告為嚴格的指引。
回溯相容性
- 這項變更不會影響 ABI 相容性。
- 如需變更解碼器或繫結,以符合這項 FTP,這些變更可能會導致來源層級 (API) 中斷,應視情況解決。
效能
- 強制解碼器和繫結符合此 FTP 可能會導致效能降低 (可能微不足道),因為系統會強制解碼器和繫結處理所有不明欄位,並關閉所有控制代碼。
- 繫結可能需要額外的間接層級 (因此會使用額外的記憶體/二進位大小),才能提供「不明」標記,以進行詳盡的標記檢查。
安全性
這項 FTP 可提升安全性。
- 我們指定含有不明內容的訊息驗證行為。
- 嚴格訊息可讓解碼器在用戶端檢查未知內容前,先驗證並捨棄這些內容,降低發生錯誤的可能性。
測試
請參閱「導入策略」一節 (我們計畫使用 FIDL 相容性測試)。此外,每種語言繫結都應有自己的測試,以判斷行為是否正確。
缺點、替代方案和未知事項
這份 FTP 大致說明瞭行為,並有相關的實作成本,可確保語言繫結符合建議。
替代做法:預設為嚴格模式或混合模式
嚴格程度應視為與向量或字串的大小界限類似,是獨立於訊息版面配置的限制,且可變更,不會造成 ABI 損壞。
我們希望 FIDL 作者明確選擇限制 (約束) 訊息。
此外,我們不希望採用混合模式,也就是部分訊息 (例如列舉) 預設為嚴格,其他訊息 (例如表格) 則否。
替代方案:使用 [嚴格] 屬性,而非新關鍵字
這個概念非常重要,值得擁有自己的關鍵字。 其他語言中類似的功能有足夠的先例,可順利轉換為 FIDL。
替代方案:其他關鍵字
在設計階段,我們提出了幾種不同的替代方案。
最有可能的候選者是 final:表示「主題的最終字詞」,在 C++、Java、C# (和其他語言) 中具有優先順序。
不過,我們可能會想在通訊協定中使用「final」關鍵字,表示無法在組合中使用該關鍵字 (即「final」的傳統用法),因此我們選擇使用另一個關鍵字來表示嚴格驗證。
這會開啟導入語法的可能性,例如:
final strict protocol Important {
MyMethod(SomeTable arg);
};
這表示通訊協定 Important 無法組成,且所有驗證都必須嚴格。
其他探索的關鍵字包括:sealed、rigid、fixed、closed、known 和 standardized。
替代方案:僅限嚴格
我們可以將所有可擴充的訊息定義為一律嚴格。目前列舉和位元都只能嚴格使用,因此這個替代方案會將其擴展至資料表和可擴充的聯集。
在這種情況下,可擴充結構的變更 (例如新增欄位) 需要先更新讀取器,再更新寫入器。這會嚴重限制這些可擴充資料結構的使用,且對較高層級的使用案例而言,限制過於嚴苛。
此外,如果這是設計選擇,我們就不需要使用資料表和可擴充聯集的外觀 (也就是不需要位元組數或控點數)。事實上,如果採用嚴格的「僅限」解讀方式,系統會拒絕不明欄位,否則結構定義會決定要使用的位元組數和控制代碼,方式與其餘訊息 FIDL 程序類似。
替代方案:僅限彈性方案
我們可以將所有可擴充的訊息定義為一律彈性。
這對列舉 (和位元) 來說非常令人意外,而且與預期相反。這會導致兩種不良的次要替代方案:
- 針對列舉 (和位元) 設下例外狀況,使其嚴格化。如上所述,這會造成混淆,且語言規則會更難理解。
- 保持這些訊息的彈性,這與預期相反,會導致錯誤 (例如讀取無效值),而且肯定會導致大量的手動編寫的普通驗證程式碼,而不是由繫結提供。
繼續探索其他可擴充訊息 (表格和可擴充聯集),嚴格性仍有空間和需求。
舉例來說,假設安全記錄通訊協定 LogEntry 定義為表格。實作這個通訊協定時,可能會想確保用戶端不會傳送伺服器無法解讀的欄位,以免這些用戶端對這些新欄位如何控管記錄檔項目的處理方式抱持期望。舉例來說,新版可能會新增「pii ranges」欄位,提供含有個人識別資訊的記錄項目範圍,且必須特別記錄 (例如以不重複的 ID 取代,原始資料則存放在該不重複的 ID 下)。為避免舊伺服器接受這類酬載,並可能誤用這些記錄項目,作者會為 LogEntry 選擇嚴格模式,以防日後遭到濫用。
既有技術和參考資料
部分基本原理是依據 go/proto3-unknown-fields,其中說明瞭 proto3 為何捨棄保留不明欄位的支援,然後又撤銷了這項決定。
- FTP-037:交易訊息標頭 v3 (尚未發布)
Footnote1
可擴充訊息中包含列舉和位元,因為訊息定義後可以新增或移除新成員。