如何解码作为UTF8编码的代理字符?

3

我的C#程序接收到一些UTF-8编码的数据,并使用 Encoding.UTF8.GetString(data) 进行解码。当产生数据的程序获得BMP范围之外的字符时,它将它们编码为2个代理字符,每个字符分别作为UTF-8编码。在这种情况下,我的程序无法正确解码。

我该如何在C#中解码这样的数据?

示例:

static void Main(string[] args)
{
    string orig = "";
    byte[] correctUTF8 = Encoding.UTF8.GetBytes(orig); // Simulate correct conversion using std::codecvt_utf8_utf16<wchar_t>
    Console.WriteLine("correctUTF8: " + BitConverter.ToString(correctUTF8));  // F0-9F-8C-8E - that's what the C++ program should've produced

    // Simulate bad conversion using std::codecvt_utf8<wchar_t> - that's what I get from the program
    byte[] badUTF8 = new byte[] { 0xED, 0xA0, 0xBC, 0xED, 0xBC, 0x8E };
    string badString = Encoding.UTF8.GetString(badUTF8); // ���� (4 * U+FFFD 'REPLACMENT CHARACTER')
    // How can I convert this?
}

注意: 编码程序是用C++编写的,并使用std::codecvt_utf8<wchar_t>进行数据转换(如下所示)。正如@PeterDuniho的答案正确指出的那样,它应该使用std::codecvt_utf8_utf16<wchar_t>。不幸的是,我无法控制此程序,也不能改变其行为 - 只能处理其格式不正确的输入。

std::wstring_convert<std::codecvt_utf8<wchar_t>> utf8Converter;
std::string utf8str = utf8Converter.to_bytes(wstr);

2
我得到的字符是0xD83C 0xDF0E,而不是你所说的0xD83D 0xDF0E。此外,如果我使用.NET将该字符编码为UTF8,我得到的是F0 9F 8C 8E,而不是你所说的ED A0 BC ED BC 8E。最后,当我将F0 9F 8C 8E解码回C#字符串时,我得到了我开始的"",并且它以UTF16编码为原始的0xD83C 0xDF0E,正如预期的那样。请提供一个好的[mcve],可靠地重现您的问题。目前,这看起来只是您的代码转换为UTF8的问题(它看起来根本不像C#...似乎是C++)。 - Peter Duniho
2
代理代码点不能被编码为UTF-8(或任何UTF),因此Encoding.UTF8.GetString正确地用U+FFFD替换无效字节。你所看到的类似于CESU-8 - 一二三
@PeterDuniho:字符已经更正,抱歉。我添加了样例,并澄清我不再控制生成程序。 - Jonathan
1个回答

3
没有一个好的最小、完整和可验证的代码示例,我们无法确定,但在我看来,您可能在使用错误的C++转换器。

std::codecvt_utf8<wchar_t> locale将从UCS-2进行转换,而不是UTF-16。这两者非常相似,但UCS-2不支持所需的替代对,以编码您想要编码的字符。

相反,您应该使用std::codecvt_utf8_utf16<wchar_t>

std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> utf8Converter;
std::string utf8str = utf8Converter.to_bytes(wstr);

当我使用该转换器时,我得到了所需的UTF-8字节:F0 9F 8C 8E。当作为UTF-8解释时,这些字节当然可以在.NET中正确解码。
附加说明:
问题已经更新,指示编码代码无法更改。您必须使用已编码为无效UTF8的UCS-2。由于UTF8是无效的,因此您必须自己解码文本。
我看到有几种合理的方法可以做到这一点。首先,编写一个解码器,不关心UTF8是否包括无效的字节序列。其次,使用C++ std::wstring_convert<std::codecvt_utf8<wchar_t>> 转换器来解码字节(例如,在C++中编写接收代码,或者编写一个C++ DLL,您可以从C#代码调用它来完成工作)。
第二个选项在某种意义上更可靠,即您正在使用创建错误数据的确切解码器。另一方面,即使创建DLL,甚至使用C++/CLI,您仍然需要一些头痛才能正确地进行交互,除非您已经是专家。
我对C++/CLI有一定了解,但并不是专家。我更擅长C#,因此这是第一种选择的一些代码:
private const int _khighOffset = 0xD800 - (0x10000 >> 10);

/// <summary>
/// Decodes a nominally UTF8 byte sequence as UTF16. Ignores all data errors
/// except those which prevent coherent interpretation of the input data.
/// Input with invalid-but-decodable UTF8 sequences will be decoded without
/// error, and may lead to invalid UTF16.
/// </summary>
/// <param name="bytes">The UTF8 byte sequence to decode</param>
/// <returns>A string value representing the decoded UTF8</returns>
/// <remarks>
/// This method has not been thoroughly validated. It should be tested
/// carefully with a broad range of inputs (the entire UTF16 code point
/// range would not be unreasonable) before being used in any sort of
/// production environment.
/// </remarks>
private static string DecodeUtf8WithOverlong(byte[] bytes)
{
    List<char> result = new List<char>();
    int continuationCount = 0, continuationAccumulator = 0, highBase = 0;
    char continuationBase = '\0';

    for (int i = 0; i < bytes.Length; i++)
    {
        byte b = bytes[i];

        if (b < 0x80)
        {
            result.Add((char)b);
            continue;
        }

        if (b < 0xC0)
        {
            // Byte values in this range are used only as continuation bytes.
            // If we aren't expecting any continuation bytes, then the input
            // is invalid beyond repair.
            if (continuationCount == 0)
            {
                throw new ArgumentException("invalid encoding");
            }

            // Each continuation byte represents 6 bits of the actual
            // character value
            continuationAccumulator <<= 6;
            continuationAccumulator |= (b - 0x80);
            if (--continuationCount == 0)
            {
                continuationAccumulator += highBase;

                if (continuationAccumulator > 0xffff)
                {
                    // Code point requires more than 16 bits, so split into surrogate pair
                    char highSurrogate = (char)(_khighOffset + (continuationAccumulator >> 10)),
                        lowSurrogate = (char)(0xDC00 + (continuationAccumulator & 0x3FF));

                    result.Add(highSurrogate);
                    result.Add(lowSurrogate);
                }
                else
                {
                    result.Add((char)(continuationBase | continuationAccumulator));
                }
                continuationAccumulator = 0;
                continuationBase = '\0';
                highBase = 0;
            }
            continue;
        }

        if (b < 0xE0)
        {
            continuationCount = 1;
            continuationBase = (char)((b - 0xC0) * 0x0040);
            continue;
        }

        if (b < 0xF0)
        {
            continuationCount = 2;
            continuationBase = (char)(b == 0xE0 ? 0x0800 : (b - 0xE0) * 0x1000);
            continue;
        }

        if (b < 0xF8)
        {
            continuationCount = 3;
            highBase = (b - 0xF0) * 0x00040000;
            continue;
        }

        if (b < 0xFC)
        {
            continuationCount = 4;
            highBase = (b - 0xF8) * 0x01000000;
            continue;
        }

        if (b < 0xFE)
        {
            continuationCount = 5;
            highBase = (b - 0xFC) * 0x40000000;
            continue;
        }

        // byte values of 0xFE and 0xFF are invalid
        throw new ArgumentException("invalid encoding");
    }

    return new string(result.ToArray());
}

我测试了您提供的地球字符,它可以正常工作。它也能正确解码该字符的UTF8编码(即F0 9F 8C 8E)。当然,如果您打算使用该代码来解码所有UTF8输入,您必须测试它以覆盖全部数据范围。


谢谢,那确实是生产者程序的正确代码。不幸的是,我无法控制它,因此我正在寻找在消费端C#方面进行修复以弥补这种行为的方法。 - Jonathan
我没有编写C++/CLI解码器,因为那会花费我更长的时间,并且95%的时间我将与与实际问题无关的内容搏斗。 :) - Peter Duniho
谢谢,这就是我一直在寻找的答案,虽然我希望在.NET框架或者一个知名库中能够找到现成的解码器... 我熟悉C++/CLI,但仅仅为了它,我们的构建系统就需要不可接受的投资,更不用说额外的DLL了。 您,先生,是一个绅士、学者和众人之王! - Jonathan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接