文件名的Unicode组合
摘要
本文讨论了在Subversion版本控制系统中,不同操作系统之间Unicode文件名组合(NFC与NFD)面临的挑战,并提出了处理这些差异的解决方案。
<p><a href="https://lobste.rs/s/g3jf3e/unicode_composition_for_filenames">评论</a></p>
查看缓存全文
缓存时间: 2026/06/12 14:54
来源: https://svn.apache.org/repos/asf/subversion/trunk/notes/unicode-composition-for-filenames
\-\*\- Text \-\*\-
内容
=======
* 上下文
* 问题描述
* 解决前的状况
- 单一平台
- 多平台:Windows + MacOS X
* 拟采用的支持库
- 假设
- 选项
* 拟采用的规范形式
* 可能的解决方案
- 在 MacOS X 上对路径输入进行规范化
- 在所有平台上对路径输入进行规范化
- (客户端)比较例程
- (全局)比较例程
* 短期(即 2.0 之前)解决方案
* 长期(即 2.0+)解决方案
* 附加信息
* 参考资料
上下文
=======
在 Unicode 中,某些带有变音符号的字符可以用两种形式表示:正则组合形式 (NFC) 或正则分解形式 (NFD)。一个 Unicode 字符字符串可以包含这两种形式的任意混合。此问题明确不涉及不可见字符、空格或其他不太可能出现在文件名中的字符。请注意,本问题明确排除 NFKC/NFKD(兼容性)规范形式,因为它们会去除格式(即它们是有损的?)。
由于 Unicode 中(某些)字符有两种表示形式,可能产生不同的码点序列来表示相同的字符序列 [1]。Subversion 内部选择的 Unicode 编码 UTF-8 将码点编码成(一系列)字节(八位字节)。由于指定某个字符的码点序列可能不同,生成的 UTF-8 也可能不同。因此,我们就有了多种方式来指定同一个路径。
下表列出了各操作系统在处理 Unicode 文件名时的行为:
| 操作系统 | 接受 | 返回 |
|----------|------|------|
| MacOS X[2] | 所有 | NFD* |
| Linux | 所有 | 如同提供 |
| Windows | 所有 | 如同提供 |
| 其他 | ? | ? |
* 在此关于完全或部分 NFD 有一些说明,但关键是:如果你发送 NFC 进去,别指望能原样拿回来!
问题描述
================
根据上述问题描述,会出现两个问题:
首先,我们通常不能依赖操作系统原样返回我们给它的文件名。这主要是一个客户端问题,可能可以在客户端库(client/subr/wc)中解决。
其次,同一个文件名可能被编码成不同的码点。这个问题比第一个更广泛,尤其考虑到我们已经有很多已有的仓库“在那里”。我们不能依赖来自操作系统的文件名——即使与仓库中的文件名不同——来命名另一个文件。这会影响仓库(即服务器端)。
解决前的状况
==============================
本节旨在描述在不同客户端/服务器操作系统组合中可能出现的问题。如上下文部分表格所示,Linux 和 Windows 的行为预期相同。因此本节不再单独考虑 Linux 系统。下面的平台严格指客户端:问题描述部分提到的服务器端问题仅涉及仓库,仓库可以位于任何服务器平台上。
单一平台
\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-
这可能是多台 MacOS X 机器或多台 Windows 机器。在这种场景下,不会出现互操作性问题。
多平台:Windows + MacOS X
\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-
假设某个文件名包含一个或多个预组合(NFC)字符,从 Windows 提交。当 MacOS X 开发者更新时,文件以 NFC 形式写入,但正如上下文部分所述,Mac 会将其重编码为 NFD。现在,当比较来自磁盘的内容(NFD)与条目文件中的内容(NFC)时,结果会发现缺少了一个文件(NFC 编码的那个)以及一个未版本控制的文件(NFD 编码的那个)。两个文件名在屏幕上看起来完全一样,但 Subversion 输出会让人困惑。[==> 造成混乱!]
反过来提交文件可能问题较小,因为 Windows 能够存储 NFD 文件名。
拟采用的支持库
=======================
假设
\-\-\-\-\-\-\-\-\-\-\-
主要假设是我们会继续使用 APR 进行字符集转换,这意味着所选的重新编码解决方案除重新编码外不需要提供其他功能。
选项
\-\-\-\-\-\-\-\-
根据我的了解 [dionisos],有两个选项可以选择支持所需功能的库:
1. International Component for Unicode (ICU)[3] —— 一个功能非常广泛的库,但其内存占用也与之成正比。为了能够使用它,我们需要显著削减该库的大小。
2. utf8proc —— 一个用于处理 UTF-8 编码 Unicode 字符串的库。它专门针对对 UTF-8 编码字符串执行有限数量的操作。它由两个 .c 文件和一个 .h 文件组成,总源代码大小为 1MB(编译后小于 0.5MB)。
在这两者中,根据上述假设,使用 utf8proc 是唯一合理的选择。
拟采用的规范形式
===================
拟采用的内部“规范形式”应为 NFC,仅仅因为它是两者中最紧凑的形式:当分配内存来存储转换结果时,永远不需要分配比输入缓冲区更大的空间。这可以使 utf8proc 获得最大性能,因为当缓冲区太小时,utf8proc 需要两次重新编码运行:一次获取所需缓冲区大小,第二次实际存储结果。
可能的解决方案
==================
有几种方案可以解决此问题,各有优缺点,概述如下。
1. 在 MacOS X 上对(路径)输入进行规范化。由于 Mac 似乎是唯一将路径名输入篡改为 NFD 的平台,这似乎是一个合乎逻辑的(低影响)解决方案。
2. 在所有平台上对(路径)输入进行规范化。如果我们对编码进行标准化,路径就只能因编码不同而不同,这似乎是一个合乎逻辑的(相对较低)影响的解决方案。
3. 在客户端和服务器上对路径输入进行规范化。在服务器端,非规范化的路径可能已经成为仓库的一部分。我们可以通过将来自仓库和客户端的任何路径都进行转换,来实现完全的内存内标准化。
4. 客户端和服务器端的路径比较例程。由于从仓库读取的路径可能用于访问该仓库(可能通过计算哈希值),来自仓库的路径不能被篡改(服务器端)。为了消除影响,我们承认我们不会“纯净”:我们总是需要路径比较例程。
方案 (1) 有一个很强的缺点:它会破坏所有现有的仅 MacOS X 工作坊。想象一个客户端,在之前所有路径都是 NFD 编码的环境中开始发送 NFC 编码路径,而服务器没有适当的支持。这将导致对仓库中路径为 NFD 编码的文件提交使用 NFC 编码路径:出现混乱。
方案 (2) 在 MacOS X 上存在与方案 (1) 相同的问题,但好处是它可以防止新的 NFD 路径进入仓库(对于足够宽泛的“客户端”定义,比如 mod_dav_svn)。
如前所述,方案 (3) 可能使路径无法被找到,如果检索机制是基于哈希的。这意味着这可能破坏任何使用哈希来存储路径信息的仓库后端。(我们不是基于哈希在 FSFS 中存储锁的吗?)
方案 (4) 没有定义内部标准表示,假设鉴于前面所有方案中发现的问题,无法保持内存内状态的纯净。相反,它要求所有路径比较都使用特殊的 NFC/NFD 编码感知函数进行。
短期解决方案
===================
由于我们的互操作性保证,客户端和服务器应被视为独立的宇宙,每个都可以使用自己的(内部)解决方案。然而,客户端应始终使用服务器发送的确切路径。反之亦然。
鉴于上述情况,短期(2.0 之前)解决方案应使用方案 (4) 中所述的路径比较例程。
长期解决方案
=================
长期(2.0+)解决方案是使用方案 (2),这确保所有输入路径都被重新编码为“正规”规范形式(NFC)。在这种情况下,不再需要使用专门的路径比较例程(尽管出于其他设计考虑可能仍需要)。
短期解决方案实现的后果
==============================================
如前所述,由于在 2.0 打破向后兼容之前,我们不知道方程的另一端是否是支持规范化的客户端或服务器,因此客户端和服务器应该能够向后兼容地与不支持规范化的“另一端”通信。因此,解决此问题意味着将客户端和服务器视为独立的宇宙,每个都可以采用自己的内部解决方案。
实现方案 (4) 意味着:
A. 使用 NFC/NFD 感知的比较函数比较文件名与条目路径。然后,当匹配时,*使用条目文件中的路径名* 与服务器通信;毕竟,该路径在被添加时可能使用了与我们从磁盘得到的不同的编码。
B. 使用 NFC/NFD 感知的比较函数匹配工作副本路径与条目文件路径。匹配时,使用条目文件路径与服务器通信。
上述意味着客户端必须非常小心地保留服务器发送的编码,并在与服务器通信时使用该编码;否则服务器可能无法将该路径识别为版本化的实体。但在本地,我们不能确定文件系统会强制使用服务器发送给客户端的编码,这意味着存在一些(人为的)情况,其中文件在本地使用的编码与仓库中的不同。因此,我们必须非常小心如何找到我们的文件,并始终使用从本地文件系统获取的编码。
实现细节:
* svn_wc_adm_access_t 中的哈希键是基于规范化路径编码进行哈希,而不是仓库路径,以便能够从工作副本路径和仓库路径计算出哈希键。
* 同样的推理适用于条目哈希中的哈希键。
新约定:
* 包含本地文件系统中编码路径的变量应包含子字符串 'wc_path'。
* 包含仓库中编码路径的变量应包含子字符串 'repo_path'。
附加信息
=====================
* "UTF-8 NFC/NFD paths issue" dev@ 邮件列表讨论:http://svn.haxx.se/dev/archive-2010-09/0319.shtml
参考资料
==========
1. UAX #15: Unicode 规范化形式 http://unicode.org/reports/tr15/
2. Apple 技术问答:VFS 中的路径编码 http://developer.apple.com/qa/qa2001/qa1173.html
3. ICU - 国际组件 for Unicode http://www-306.ibm.com/software/globalization/icu/index.jsp
4. utf8proc - 处理 UTF-8 编码 Unicode 字符串的库 http://www.flexiguided.de/publications.utf8proc.en.html
相似文章
Unicode 字符串的等价性很奇怪 (2016)
Unicode 字符串等价性很复杂,尤其是涉及校对规则时,会导致意外的结果,例如删除控制字符和非确定性分组。作者讨论了在数据库系统中正确实现 Unicode 支持所面临的挑战。
GNU IFUNC 才是 CVE-2024-3094 真正的罪魁祸首
本文认为,GNU IFUNC 以及将 OpenSSH 链接到 SystemD 的设计决策,才是 CVE-2024-3094 xz-utils 后门漏洞得以实施的主要促成因素,而非恶意代码本身。
使用NFS将Git提交挂载为文件夹
Julia Evans 创建了一个名为 git-commit-folders 的工具,它使用 NFS(和 FUSE)将 Git 提交挂载为文件夹,让用户可以像浏览目录一样探索旧的提交。
使用 Fedora Silverblue 进行合成器开发
本指南介绍如何使用原子发行版 Fedora Silverblue 开发 niri Wayland 合成器,重点介绍了不可变系统在系统组件开发中的优势。
终端文本渲染的各种缺陷(2024)
本文探讨了终端模拟器中文本渲染的各种根本性问题,包括字符定义歧义、Unicode处理问题、有缺陷的二维网格假设以及光标不同步,凸显了支持复杂脚本和字体的困难。