当试图读取不同编码的文件时,用MSVC编译的C程序崩溃,然后系统区域设置

rn0zuynd  于 2023-04-29  发布在  其他
关注(0)|答案(2)|浏览(98)

背景

程序使用系统区域设置setlocale(LC_ALL, "");。通常在Windows上,这意味着一些单字节编码(取决于Windows的语言版本):Windows-1250、Windows-1252、拉丁美洲、....
现在输入文件是在不同的编码(以免假设UTF-8)。程序应正确加载此文件到内存中,并使用系统区域设置提供的编码打印它。

MCVE

以下是最小完全可验证示例:

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>

void copyTextFiles(FILE* in, FILE* out)
{
    char buff[1024];
    while(fscanf(in, "%1023[^\n]%*[\n]", buff) == 1) {
        fprintf(out, "%s\n", buff);
    }
}

int main(int argc, char* argv[])
{
    char * l = setlocale(LC_ALL, "");
    if (!l) {
        perror("setlocale");
        return 1;
    }
    printf("Locale selected: %s\n", l);
    FILE *file = fopen("example_utf-8.txt", "rt+, ccs=UTF-8");
    if (!file) {
        perror("example_utf-8.txt");
        return 2;
    }
    printf("File was opened\n");
    copyTextFiles(file, stdout);
    fclose(file);
    printf("Done\n");
}

一些随机输入文件example_utf-8.txt

ąłóę
ąłóę
+−-😀😃
ÀÁÂÃĀĂȦÄẢ
ḂƁƇȨɃÆǼĶɈǨỒỐŔṘȟḧƕ
ɋƥṗ

问题

此代码与MSVC文档一致:
fopen,_wfopen|微软学习

Unicode支持

fopen支持Unicode文件流。要打开Unicode文件,请将指定所需编码的ccs=encoding标志传递给fopen,如下所示。

FILE *fp = fopen("newfile.txt", "rt+, ccs=UTF-8");

ccs编码的允许值为UNICODEUTF-8UTF-16LE

但程序崩溃,调用堆栈如下:

>   ucrtbased.dll!__acrt_stdio_char_traits<char>::validate_stream_is_ansi_if_required(_iobuf * const stream) Line 495   C++
    ucrtbased.dll!__crt_stdio_input::stream_input_adapter<char>::validate() Line 98 C++
    ucrtbased.dll!__crt_stdio_input::input_processor<char,__crt_stdio_input::stream_input_adapter<char>>::process() Line 1092   C++
    ucrtbased.dll!common_vfscanf::__l2::<lambda>() Line 47  C++
    ucrtbased.dll!__crt_seh_guarded_call<int>::operator()<void <lambda>(void),int <lambda>(void) &,void <lambda>(void)>(__acrt_lock_stream_and_call::__l2::void <lambda>(void) && setup, common_vfscanf::__l2::int <lambda>(void) & action, __acrt_lock_stream_and_call::__l2::void <lambda>(void) && cleanup) Line 202 C++
    ucrtbased.dll!__acrt_lock_stream_and_call<int <lambda>(void)>(_iobuf * const stream, common_vfscanf::__l2::int <lambda>(void) && action) Line 297   C++
    ucrtbased.dll!common_vfscanf<char>(const unsigned __int64 options, _iobuf * const stream, const char * const format, __crt_locale_pointers * const locale, char * const arglist) Line 36    C++
    ucrtbased.dll!__stdio_common_vfscanf(unsigned __int64 options, _iobuf * stream, const char * format, __crt_locale_pointers * locale, char * arglist) Line 60    C++
    read_file.exe!_vfscanf_l(_iobuf * const _Stream, const char * const _Format, __crt_locale_pointers * const _Locale, char * _ArgList) Line 1068  C
    read_file.exe!fscanf(_iobuf * const _Stream, const char * const _Format, ...) Line 1210 C
    read_file.exe!copyTextFiles(_iobuf * in, _iobuf * out) Line 8   C
    read_file.exe!main(int argc, char * * argv) Line 28 C
    [External Code]

调用堆栈顶部指向_VALIDATE_STREAM_ANSI_RETURN

template <>
struct __acrt_stdio_char_traits<char> : __crt_char_traits<char>
{
    static int_type const eof = EOF;

    static bool validate_stream_is_ansi_if_required(FILE* const stream) throw()
    {
        _VALIDATE_STREAM_ANSI_RETURN(stream, EINVAL, false);
        return true;
    }
};

因此,在C库下面使用的是C++,但我不清楚这种检查到底是做什么的,以及为什么会失败(崩溃)。

CPP版本

#include <iostream>
#include <fstream>
#include <string>
#include <locale>

int main()
{
    std::locale sysLocale("");
    std::locale utfLocale(".utf-8");
    std::locale::global(utfLocale);
    std::cout.imbue(sysLocale);
    std::cout << "Locale: " << sysLocale.name() << '\n';
    
    std::ifstream file("example_utf-8.txt");
    if (!file) {
        std::perror("example_utf-8.txt");
        return 2;
    }
    file.imbue(utfLocale);
    std::string line;
    while (getline(file, line)) {
        std::cout << line << '\n';
    }
    std::cout << "\nDONE\n";
}

它工作得很好,如果系统语言环境不支持特定字符,它可以转换字符,下面是cmd.exe的输出:

C:\repo\mixEncodingsInC\build\Debug>chcp 65001

C:\repo\mixEncodingsInC\build\Debug>cpp_read_file.exe
Locale:
ąłóę
ąłóę
+−-😀😃
ÀÁÂÃĀĂȦÄẢ
ḂƁƇȨɃÆǼĶɈǨỒỐŔṘȟḧƕ
ɋƥṗ

DONE

C:\repo\mixEncodingsInC\build\Debug>chcp 437
Active code page: 437

C:\repo\mixEncodingsInC\build\Debug>cpp_read_file.exe
Locale:
alóe
alóe
+--????
AAAAAA?Ä?
?????Æ?K?K??R????
???

DONE

C:\repo\mixEncodingsInC\build\Debug>chcp 1252
Active code page: 1252

C:\repo\mixEncodingsInC\build\Debug>cpp_read_file.exe
Locale:
alóe
alóe
+--????
ÀÁÂÃAA?Ä?
?????Æ?K?K??R????
???

DONE

提问

为什么会崩溃?我是不是误解了文件中的一些内容?我能做点什么让它工作吗?

注意事项

x6492ojm

x6492ojm1#

这样修改C代码很有趣:

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>

void copyTextFiles(FILE* in, FILE* out)
{
    char buff[1024];
    while(fscanf(in, "%1023[^\n]%*[\n]", buff) == 1) {
        fprintf(out, "%s\n", buff);
    }
}

int main(int argc, char* argv[])
{
    char * l = setlocale(LC_ALL, ".utf-8");
    if (!l) {
        perror("setlocale");
        return 1;
    }
    printf("Locale selected: %s\n", l);
    FILE *file = fopen("example_utf-8.txt", "rt+");
    if (!file) {
        perror("example_utf-8.txt");
        return 2;
    }

    printf("File was opened\n");
    copyTextFiles(file, stdout);
    fclose(file);
    printf("Done\n");
}

即使系统区域设置(代码页)发生更改,也可以在控制台中打印正确的字符。
所以看起来printf考虑了系统区域设置和程序使用的setlocale集编码(所以它类似于cpp示例中的std::locale::global(utfLocale);)。
问题仍然存在,我想读取不同编码的文件,然后由应用程序使用。

fhity93d

fhity93d2#

MSVC文档和库中的所有UTF-8似乎都是由一群猴子编写的。例如:
FILE *fp = fopen(“newfile.txt”,“rt+,ccs=UTF-8”);
ccs编码允许的值为UNICODE、UTF-8和UTF-16 LE。
当以Unicode模式打开文件时,输入函数将从文件中读取的数据转换为UTF-16数据,存储为类型wchar_t。写入以Unicode模式打开的文件的函数需要包含UTF-16数据的缓冲区,这些数据存储为wchar_t类型。如果文件编码为UTF-8,那么UTF-16数据在写入时将转换为UTF-8。文件的UTF-8编码内容在读取时被转换为UTF-16。尝试在Unicode模式下读取或写入奇数字节会导致参数验证错误。
这看起来像是wfopen文档中的直接复制粘贴。UTF-16这一点在狭义的字符环境中没有任何意义。
但是如果我们仔细观察,我们可以推断出ccs=UTF-8的意思是“读取为UTF-8 * 并转码为我当前的编码,无论它是什么 *”(如果使用宽流,则可能是UTF-16,如果不是,则可能是任何“ANSI”代码页)。但是为什么代码转换失败会导致崩溃是我无法理解的。这使得ccs=UTF-8,至少在读取模式下,在任何现实生活中的软件中都是不可用的。问问微软他们是怎么想的无论如何,这种行为不是你想要的。文档中的下一句话告诉您要做什么(但是考虑到文档的整体质量,请自己验证)。
若要读取或写入以UTF-8格式存储在程序中的数据,请使用文本或二进制文件模式而不是Unicode模式。您负责任何所需的编码翻译。

相关问题