在Windows上使用zlib处理Unicode文件路径

10

我正在使用zlib读取gzip压缩文件。 然后你使用以下代码打开一个文件:

gzFile gzopen(const char *filepath, const char *mode);

在Windows平台上,如果文件路径以const wchar_t*的形式存储,该如何处理Unicode文件路径?

在类UNIX平台上,您只需将文件路径转换为UTF-8并调用gzopen()即可,但这在Windows上不起作用。


3
在Windows操作系统上,默认情况下,wcstombs函数将字符串转换为Windows-1252编码。对于无法表示为Windows-1252编码的字符,将被替换为不同的替换字符。如果发生这种情况,转换后的字符串将无法用作文件路径。 - Johan Råde
1
@Hans Passant:我正在编写一个库,其接口以boost::filesystem::path形式接受文件路径,其实现可能使用ZLib库读取文件。那么这是一个问题。 - Johan Råde
3
@Hans Passant:我读了这个,但我不理解你的意思。在我Windows电脑上,我可以创建名称为“黒死.txt”的文件。并且我可以通过将其名称(作为UTF-16编码的宽字符串)传递给_wfopen(...)来打开该文件。 - Johan Råde
再次强调,这是一个由创建gzip的人所面临的问题。你可以很容易地假设,在东亚gzip并不是一种非常流行的格式。 - Hans Passant
问题在于你的代码和操作系统之间的某个库需要一个 char *。(至少这是我的问题,也是我今天来这里的原因)。因此,必须有一种方法可以将 wchar* → [lib space] char*fopen → [os space] _wfopen,最终的 _wfopen 具有原始字符串的重构。所以问题是,dan04 的 ToUTF16 的反函数是什么?是 wcstombs 吗?在从我的代码到操作系统空间的链中,没有必要将字符串解释为字形,因此无法编码的字符可以保留为 mbs,并通过 ToUTF16 重构。 - v.oddou
显示剩余8条评论
5个回答

15
下一个版本的zlib将包含此函数,其中定义了_WIN32gzFile gzopen_w(const wchar_t *path, char *mode); 它的工作方式与gzopen()相同,只是它使用_wopen()而不是open()
我故意没有复制_wfopen()的第二个参数,因此我没有将其称为_wgzopen(),以避免可能与该函数参数混淆。 因此名称为gzopen_w()。 这也避免了使用C保留名称空间。

2
最好的回答。你问如何做,库的作者就会提供一个新功能。 - Erkin Alp Güney
@ErkinAlpGüney 我不同意,更好的选择是使用问题追踪器。顺便说一下:这个“下一个版本”是 **1.2.7 (2012年5月2日)**(参见该版本的第一个提交,问题追踪器中没有相应的条目)。 - Wolf

12

首先,什么是文件名?

在类Unix系统中

文件名是由零终止的一系列字节。内核不需要关心字符编码(除了知道“/”的ASCII代码)。

然而,从用户的角度来看,将文件名解释为字符序列更加方便,这通过作为本地环境的一部分指定的字符编码来完成。通过提供UTF-8本地环境来支持Unicode

在C程序中,文件使用普通的char*字符串表示,如fopen函数。 POSIX API没有宽字符版本。如果您有一个wchar_t*文件名,必须明确地将其转换为char*

在Windows NT上

文件名是由一系列UTF-16代码单位组成。实际上,Windows中所有的字符串操作都在内部使用UTF-16进行。

微软的C/C++库,包括Visual C++运行库,都遵循这样的约定:char* 字符串使用特定于区域设置的传统“ANSI”代码页表示,而wchar_t*字符串则以UTF-16方式表示。并且char*函数只是新的wchar_t*函数的向后兼容包装器。

因此,如果您调用MessageBoxA(hwnd, text, caption, type),那基本上等同于调用MessageBoxW(hwnd, ToUTF16(text), ToUTF16(caption), type)。当您调用 fopen(filename, mode) 时,这就像_wfopen(ToUTF16(filename), ToUTF16(mode))

请注意,_wfopen 是许多非标准的 C 函数之一,用于处理 wchar_t* 字符串。这不仅仅是为了方便; 你无法使用标准的 char* 等效函数,因为它们限制了你只能使用“ANSI”代码页(其中不能是 UTF-8)。例如,在 windows-1252 区域设置中,您无法(轻松地)fopen 文件 שלום.c,因为在窄字符串中没有办法表示这些字符。

在跨平台库中

一些典型的方法包括:

  1. 使用带有char*字符串的标准C函数,不必担心Windows上对非ANSI字符的支持。
  2. 使用char*字符串,但将其解释为UTF-8而不是ANSI。在Windows上,编写包装器函数以接受UTF-8参数,将它们转换为UTF-16,并调用诸如_wfopen之类的函数。
  3. 无论何时都使用宽字符字符串,这类似于#2,但您需要为-Windows系统编写包装器函数。

zlib如何处理文件名?

不幸的是,它似乎使用上述的天真方法#1,直接使用open(而不是_wopen)。

如何解决这个问题?

除了已经提到的解决方案(我最喜欢的是Appleman1234的gzdopen建议),您还可以利用符号链接给文件一个替代的全ASCII名称,然后安全地传递给gzopen。如果文件已经有一个合适的短名称,您甚至可能不必这样做。

3
对于zlib,有两种方法。一是在Windows上使用gzopen()始终执行UTF-8到UTF-16转换并使用_wopen(),二是保留gzopen()使用open(),仅在Windows上添加一个新的_wgzopen()函数,它接受一个UTF-16参数并使用_wopen()。dan04会推荐哪一种方法? - Mark Adler
@Mark:你不应该询问gzip库是否应该使用这种或那种编码。库不应该决定使用哪种编码,这是使用库的应用程序的责任。通常,应用程序通过设置当前区域设置来完成这一点。库应该简单地使用当前区域设置指定的编码。最简单的方法是委托给现有的区域设置感知函数,就像你的备选方案b)中所示。 - Johan Råde
1
@MarkAdler:对于来说,如果zlib使用UTF-8编码将更加方便,因为这是我们团队的编码标准所要求的(主要是为了与其他第三方库(如SQLite和TinyXML)兼容)。也许您可以提供UTF-8和UTF-16版本的函数。 - dan04
2
好的。那么我可以两者兼顾。gzopen() 可以在 Windows 编译时将 UTF-8 转换为 UTF-16 并调用 _wopen。还可以有一个 _wgzopen(),它使用 UTF-16 作为输入(对于两个参数都是如此?)。我不明白整个“委托给现有区域设置感知函数”的意思。这是否意味着从 UTF-8 到 UTF-16 的转换例程不是“区域设置感知的”?顺便问一下,那个例程是什么? - Mark Adler
1
@MarkAdler:那个例程是 MultiByteToWideChar() 或者 iconv() - dan04

4
您有以下选项。
 #ifdef _WIN32 

 #define F_OPEN(name, mode) _wfopen((name), (mode))

 #endif    
  1. 在Windows上使用_wfopen替代fopen,要打补丁到zlib中,可以参照zutil.h文件的内容。

  2. 使用_wfopen_wopen替代gzopen,并将返回值传递给gzdopen

  3. 使用libiconv或其他库将文件编码从Unicode转换为ASCII,并将ASCII字符串传递给gzopen。如果libiconv失败,则处理错误并提示用户重命名文件。

有关iconv的更多信息,请访问iconv示例。该示例将日语转换为UTF-8,但将目标编码更改为ASCII或ISO 8859-1不会太困难。

有关zlib和非ANSI字符转换的更多信息,请参见此处


有人可以帮忙解读一下这里链接指向的内容吗? - Wolf
这个页面似乎没有被存档在Archive.org上,我相信它是旧的bsnes开发论坛上的一篇帖子,更详细地阐述了在bsnes日志中列出的行项目下所做的工作——修改zlib以支持非ANSI字符。链接为https://static.hexostum.net/bsnes/bsnes_changelog.txt。 - Appleman1234

3
这是 Appleman 方案 #2 的实现。代码已经过测试。
#ifdef _WIN32

gzFile _wgzopen(const wchar_t* fileName, const wchar_t* mode)
{
    FILE* stream = NULL;
    gzFile gzstream = NULL;
    char* cmode = NULL;         // mode converted to char*
    int n = -1;

    stream = _wfopen(fileName, mode);

    if(stream)
        n = wcstombs(NULL, mode, 0);
    if(n != -1)
        cmode = (char*)malloc(n + 1);
    if(cmode) {
        wcstombs(cmode, mode, n + 1);
        gzstream = gzdopen(fileno(stream), cmode);
    }

    free(cmode);
    if(stream && !gzstream) fclose(stream);
    return gzstream;
}

#endif

我将文件名模式都改成了const wchar_t*,以保持与Windows函数的一致性,例如:

FILE* _wfopen(const wchar_t* filename, const wchar_t* mode);

已经通过Visual Studio 2010编译进行了测试,在调试模式下,当应用程序即将终止时会出现异常。这可能是因为使用_wfopen打开文件,但在之后由_close关闭句柄导致的。通过dup:licating fileno,然后close:ing file,可以获得“安全”的实现,但我在下面附上了自己的函数实现。 - TarmoPikaro
在应用程序执行时出现了一些奇怪的情况。应用程序正常终止,但是在托盘图标中,我注意到某个错误报告正在自动发送给微软 - 这是我第一次看到“静默”错误报告。对最终用户没有显示任何内容。 - TarmoPikaro
在vs2012中进行了调试,应用程序终止后显示异常 - 您正在尝试终止应用程序,但调试器会挂起半分钟。此 bug 还需要重新启动 vs。 - TarmoPikaro

1

这是我自己的Unicode辅助函数版本,测试结果比上面的版本稍微好一些。

static void GetFlags(const char* mode, int& flags, int& pmode)
{
    const char* _mode = mode;

    flags = 0;      // == O_RDONLY
    pmode = 0;      // pmode needs to be obtained, otherwise file gets read-only attribute, see 
                    // https://dev59.com/20nSa4cB1Zd3GeqPM1Br

    for( ; *_mode ; _mode++ )
    {
        switch( tolower(*_mode) )
        {
            case 'w':
                flags |= O_CREAT | O_TRUNC;
                pmode |= _S_IWRITE;
                break;
            case 'a':
                flags |= O_CREAT | O_APPEND;
                pmode |= _S_IREAD | _S_IWRITE;
                break;
            case 'r':
                pmode |= _S_IREAD;
                break;
            case 'b':
                flags |= O_BINARY;
                break;
            case '+':
                flags |= O_RDWR;
                pmode |= _S_IREAD | _S_IWRITE;
                break;
        }
    }

    if( (flags & O_CREAT) != 0 && (flags & O_RDWR) == 0 )
        flags |= O_WRONLY;
} //GetFlags


gzFile wgzopen(const wchar_t* fileName, const char* mode)
{
    gzFile gzstream = NULL;
    int f = 0;
    int flags = 0;
    int pmode = 0;

    GetFlags(mode, flags, pmode);

    f = _wopen(fileName, flags, pmode );

    if( f == -1 )
        return NULL;

    // gzdopen will also close file handle.
    gzstream = gzdopen(f, mode);
    if(!gzstream)
        _close(f);
    return gzstream;
}

1
Mark在最近的zlib版本中增加了对Windows宽字符文件名的支持。 - Johan Råde
我同意。但是如果您已经集成了zlib库,并且不想麻烦重新集成新库(向后兼容性,新功能等),那么更容易的方法就是包装现有的库。 - TarmoPikaro
无论如何,您都必须将 O_BINARY 传递给 _wopen,而不仅仅是在未压缩的数据为二进制时,否则它会破坏压缩输出中的任何 0x10! - Iziminza

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