Home
26 Oct 2020

Filesystem & Unicode Normalization

Unicode Decomposed and Precomposed Characters

部份 unicode 字元有組合字元Decomposed Characters預組字元Precomposed Characters兩種表示方法,但這兩種表示方法並不是單純的一對一對應。

以 Å 為例:

另外一個複雜一點的例子,ᾅ 字元:


Unicode Equivalence and Normalization

由於 U+212B, U+00C5, U+0041-U+030A 都長成 Å 的模樣,應用程式處理 unicode 時必須考慮到等價性Equivalence,否則在比對時會找不到在視覺上無法區分的字形。但其實也不是看起來一樣就代表等價,因此產生了惡搞的空間,也額外衍生出安全性的問題。

正規化Normalization可將所有等價的序列產生一個「唯一」的序列,因此字串要正規化完才能進行比較。先不考慮相容等價,Unicode 有 NFC 和 NFD 兩種標準正規化的方法:

NFD Normalization Form Canonical Decomposition

以標準等價方式來分解

  • Å ⟶ U+0041-U+030A
  • ᾅ ⟶ U+03B1 U+0314 U+0301 U+0345

NFC Normalization Form Canonical Composition

以標準等價方式來分解,然後以標準等價重組之。重組結果有可能和分解前不同。

  • Å ⟶ U+0041-U+030AU+00C5
  • ᾅ ⟶ U+03B1 U+0314 U+0301 U+0345U+1F85

Filesystem Implemetations

先講結論,大部分的檔案系統都不會對檔案名稱做 unicode 正規化 (NFC/NFD)。因此不要假設檔案名稱是 NFC 或 NFD 形式,檔案名稱可能還沒有正規化,或者同一個檔名有兩者混合的形式,甚至根本不是合法的 unicode 編碼,就只是一坨 raw bytes 從硬碟上被讀起來。

所以系統內任何對檔案名稱的操作,主要是 create 與 lookup,如果沒有統一的正規化來規範等價性,有機會找不到存在的檔案而無法正常操作。

Btrfs

保持檔案名稱原本的形式,不做額外的 unicode 正規化。

Ext4

保持檔案名稱原本的形式,不做額外的 unicode 正規化。Linux-5.2 導入的 case-insensitive 會使用 NFD 正規化後再做檔名比較,但仍不改變檔案名稱儲存的形式。

HFS+

Mac 鍵盤輸入法會產生 NFD (precomposed) 形式的字元。

Mac 專屬的 HFS+ 是少數會對檔案名稱加工的檔案系統,儲存前一律轉成 decomposed unicode 字元,但並非嚴格的 NFD

For example, HFS Plus (Mac OS Extended) uses a variant of Normal Form D in which U+2000 through U+2FFF, U+F900 through U+FAFF, and U+2F800 through U+2FAFF are not decomposed (this avoids problems with round trip conversions from old Mac text encodings).

Linux 版本的 HFS+ 實作預設也是一樣的行為,檔案名稱會先轉成 NFD 形式再儲存,但讀取則會配合 linux 轉成 NFC 形式。此外,Linux 實作也提供了 nodecompose 掛載選項,可以關閉自動轉成 NFD 形式的正規化,保持檔案名稱原本的形式。但如果有跨平台使用的需求,請勿開啟該選項。

APFS

雖然 APFS 被定位用來取代 HFS+,但儲存檔案名稱時並不做任何 unicode 正規化,僅在算 filename hash 時使用到 NFD。

NTFS

Windows 提供了各種正規化的機制,原生從 Windows 產生的 unicode 字元會是 NFC 形式,但仍然有機會從其他外部管道(網頁)獲得 NFD 形式的 unicode 字元

Windows, Microsoft applications, and the .NET Framework generally generate characters in form C using normal input methods. For most purposes on Windows, form C is the preferred form. For example, characters in form C are produced by Windows keyboard input. However, characters imported from the Web and other platforms can introduce other normalization forms into the data stream.

NTFS 使用 unicode 的 UTF-16 編碼儲存檔案名稱,但本身並不會對檔案名稱做任何 unicode 正規化。

exFAT

exFAT 也是使用 UTF-16 編碼儲存檔案名稱。Samsung 貢獻的 Linux exFAT 實作也僅做 UTF-8 🡘 UTF-16 之間的轉換,不做任何正規化。

FAT family

FAT 家族的檔名機制因為歷史因素比較複雜,FAT16 之前僅支援 8.3 檔名,需搭配 codepage 處理。FAT32 之後支援的長檔名則使用 UTF-16 編碼,不做任何 unicode 正規化。

How to Verify

Å 的三種表達方式雖然在 unicode 標準等價下視為相等,但大部份的檔案系統不處理 unicode 等價性,所以可以用下述的方法,建立出三個長得一樣的檔案名稱。

  • U+00C5 : NFC
  • U+212B : not normalized
  • U+0041-U+030A : NFD
# touch $(echo -e "\u00C5")
# touch $(echo -e "\u212B")
# touch $(echo -e "\u0041\u030A")
# ls
# ls
./  ../  Å  Å  Å

目前已知不會建出三個檔案的檔案系統有:

  • HFS+: 不意外
  • Ext4 with case-insensitive: Ext4 比較大小寫前會先 NFD 再做 folding,因此只會建出第一個檔案,後續檔案會配判斷已存在而不建立。檔案名稱仍會保留原本的格式,不過 unicode 正規化。

It should be NFC. NFD and NFKD are most useful for internal processing.

  • Unicode.org mentions:
    • NFC is the best form for general text, since it is more compatible with strings converted from legacy encodings. NFD and NFKD are most useful for internal processing.
    • Much legacy data is automatically in NFC, since the character sets are constrained to that.
    • All user-level comparison should behave as if it normalizes the input to NFC. Most binary character matching that affects users should also behave as if it normalizes the input to NFC. Because it is rare to have non-NFC text, an optimized implementation can do such comparison very quickly.
    • Much of the existing content on the internet is already in NFC, and does not require re-normalization in contexts expecting to use NFC.
  • W3C recommends the use of NFC normalized text on the Web.
  • Windows generate characters in NFC using normal input methods.
  • Apple developer document recommends to use NFC (precomposed) when you interact with other platforms.
    • If you implement a network protocol which is defined to use precomposed Unicode.
    • When creating a cross-platform file (or volume) whose specification dictates precomposed Unicode.
    • If you incorporate a large body of cross-platform code into your application, where that code is expecting precomposed Unicode.
  • NFC is the preferred form for Linux.找不到依據

References

cccheng at 2020-10-26 1603670400