为什么 Mac OS 上的 C 运行时允许预组合和分解的 UTF-8?

Why does the C runtime on Mac OS allow both precomposed and decomposed UTF-8?

所以我们都知道 Mac OS 上的文件系统具有使用完全分解的 UTF-8 的古怪功能。例如,如果您像 realpath() 一样调用 POSIX API,您将从 Mac OS 返回这样一个完全分解的 UTF-8 字符串。但是,当使用像 fopen() 这样的 API 时,传递预组合的 UTF-8 似乎也能正常工作。

这是一个小的演示程序,它试图打开一个名为 ä 的文件。对 fopen() 的第一次调用传递了一个预组合的 UTF-8 字符串,第二次调用传递了一个分解的 UTF-8 字符串,令我惊讶的是两者都有效。我希望只有第二个可以工作,但预组合的 UTF-8 也可以。

#include <stdio.h>

int main(int argc, char *argv[])
{
    FILE *fp, *fp2;

    fp = fopen("\xc3\xa4", "rb");       // ä as precomposed UTF-8
    fp2 = fopen("\x61\xcc\x88", "rb");  // ä as decomposed UTF-8

    printf("CHECK: %p %p\n", fp, fp2);

    if(fp) fclose(fp);
    if(fp2) fclose(fp2);

    return 0;
}

现在回答我的问题:

  1. 这是定义的行为吗?即是否允许将预组合的 UTF-8 传递给 POSIX API,还是我应该始终传递分解的 UTF-8?

  2. fopen() 之类的函数如何知道传递的文件是否包含预合成或分解的 UTF-8?这难道不会导致各种问题,例如因为传递的字符串可以用两种不同的方式解释并因此可能指向两个不同的文件,所以打开了错误的文件?这让我有些困惑。

编辑

为了使混淆更加彻底,这种奇怪的行为似乎甚至不限于文件 I/O。看看这段代码:

#include <stdio.h>

int main(int argc, char *argv[])
{
    printf("\xc3\xa4\n");
    printf("\x61\xcc\x88\n");

    return 0;
}

两个 printf 调用完全相同,即它们都打印字符 ä,第一个调用使用预组合的 UTF-8,第二个调用使用分解的 UTF-8。真的很奇怪。

Unicode 字符串中有两种不同类型的等价:一种是规范等价,另一种是兼容性。由于您的问题是关于软件似乎认为相同的字符串,让我们关注 规范等价 (OTOH,兼容性 允许语义差异,所以这个问题离题了)。

引用维基百科Unicode equivalence

Code point sequences that are defined as canonically equivalent are assumed to have the same appearance and meaning when printed or displayed. For example, the code point U+006E (the Latin lowercase "n") followed by U+0303 (the combining tilde "◌̃") is defined by Unicode to be canonically equivalent to the single code point U+00F1 (the lowercase letter "ñ" of the Spanish alphabet). Therefore, those sequences should be displayed in the same manner, should be treated in the same way by applications such as alphabetizing names or searching, and may be substituted for each other.

换句话说,如果两个字符串规范等价,软件应该认为这两个字符串代表完全相同的东西。所以,MacOS 在这里做的是正确的事情:你有两个不同的 UTF-8 字符串(一个分解,另一个预组合),但它们规范等价,所以它们映射到同一个对象(在您的示例中使用相同的文件名)。这是正确的(记住上面引述中的 "should be treated in the same way by applications such as alphabetizing names or searching, and may be substituted for each other" 行)。

我不太理解你关于 printf() 的第二个例子。是的,分解字符和预合成字符呈现相同的输出。这正是 Unicode 支持的字符双重表示的要点:您可以选择是用预先组合的字节序列表示组合字符,还是用分解的字节序列表示。它们打印出相同的视觉结果,但它们的表现形式不同。如果两种表示规范等价(在某些情况下它们是,在某些情况下它们不是),那么系统必须将它们视为同一对象的两种表示。

为了在您的软件中更舒适地管理所有这些,您应该 normalize your Unicode strings 在使用它们之前。