Cocoa 的 NSString 解码错误处理

在使用 Safari 的时候,我们会注意到一个很常见的乱码问题,如下图:

Safari Decode Error

这是在打开 http://att.newsmth.net/att.php?p.719.214628.536.png 这样的图片链接时,Safari 错误的判断了这个图片文件的文件名造成的。而为什么会有这样的错误判断呢?

其实 Safari 使用的是 Cocoa 框架 URL Loading 架构中的 NSURLResponse 类的 suggestedFilename 方法实现的。

而这个方法,其实就是解析 HTTP 首部中的 Content-Disposition 域里的 filename 部分完成的,比如下面这个首部:

$ curl -I http://att.newsmth.net/att.php?p.719.214628.536.png
HTTP/1.1 200 OK
....
Content-Disposition: inline;filename=ͼƬ_6.png
....

显然这是乱码,可奇怪的是,这和我们在上面的图中看到的乱码又不一样,这是为什么呢?

假如将它作为 GBK 来解码就清楚了:

$ curl -I http://att.newsmth.net/att.php?p.719.214628.536.png | iconv -f gbk -t utf-8
HTTP/1.1 200 OK
....
Content-Disposition: inline;filename=图片_6.png
....

哦,原来是 GBK 编码的“图片_6.png”,可是这个文件名怎么会变成开头图片中那种形式的乱码呢?其实写一段 Cocoa 程序就可以发现:

#import <Foundation/Foundation.h>

int main()
{
    const char *bytes = "图片_6.png";
    NSString *str = [[NSString alloc] initWithCString: bytes
                                             encoding: NSASCIIStringEncoding];
    NSLog(@"str: %@", str);
    [str release];

    return 0;
}

(用 GBK 编码保存) 这个程序的执行结果就是输出开头那段乱码,原来 NSURLResponse 把 Content-Disposition 中的 filename 当成 ASCII 处理了,怪不得会乱码。

可是也不能怪 NSURLResponse,毕竟服务器没有提供任何编码的信息,而 RFC 2183 中也明确说明,不应该在 filename 中使用任何 ASCII 以外的字符,用了就是后果自负了。

那假如我们要写一个自己的客户端 (或者尝试修正 Safari 的错误行为),该怎么修正已经被按照 ASCII 错误解码的 NSString 呢?

因为 NSString 本身是按照 UTF-16 编码的,所以如果逐个字符地观察这个错误解码后的 NSString:

int max = [str length];

int i;
for (i = 0; i < max; i++)
{
    unichar ch = [str characterAtIndex: i];
    printf("%x ", ch);
}

我们可以得到:

cd bc c6 ac 5f 36 2e 70 6e 67

这样一串输出,5f 36 2e 70 6e 67 就是 _6.png,比较好认,前面的 cd bc c6 ac 是什么呢?一查,原来是“图”和“片”这两个字的 GBK 编码。

这就好理解了:NSString 一开始把一段 GBK 编码的字节流逐个字节地按照 8bit-ASCII 处理了,原本 10 个字节对应的是 7 个字符,结果被错误地解码为了 10 个字符,所以我们要把它转换回去,首先是要还原回原来的那段字节流:

int max = [str length];
char *nbytes = malloc(max + 1);

int i;
for (i = 0; i < max; i++)
{
    unichar ch = [str characterAtIndex: i];
    nbytes[i] = (char) ch;
}
nbytes[i] = '\0';

然后再将这段字节流按照正确的编码 (GB18030) 处理:

NSStringEncoding enc = CFStringConvertEncodingToNSStringEncoding(
    kCFStringEncodingGB_18030_2000);

NSLog(@"nstr: %@", [NSString stringWithCString: nbytes
                                      encoding: enc]);

结果果然得到了正确的输出:

2008-02-17 06:09:46.408 test[14095:10b] nstr: 图片_6.png

同样的逻辑可以用在很多类似的乱码情形中。完整的代码可以在这里下载: test-str-encoding.m

Author: jjgod

A software engineer from China, working on text rendering for a fruit company. Interested in typography and science fiction.

2 thoughts on “Cocoa 的 NSString 解码错误处理”

  1. @fishy: 水木是写成 GBK 形式的,变成 &#xxx; 是因为 Terminal 把 GBK 的按照 UTF-8 解码出现的乱码用 GBK 表达不出来 (我的 blog 数据库本身是 GBK 的),被 blog 程序转换的。你在 Terminal 下试一下那个命令就可以看到那段乱码了。

Leave a Reply

Your email address will not be published. Required fields are marked *