为什么JSON :: XS不生成有效的UTF-8?

时间:2014-12-06 19:56:29

标签: json perl utf-8

我收到了一些损坏的JSON,我已将其减少到这个测试用例。

use utf8;
use 5.18.0;
use Test::More;
use Test::utf8;
use JSON::XS;

BEGIN {
    # damn it
    my $builder = Test::Builder->new;
    foreach (qw/output failure_output todo_output/) {
        binmode $builder->$_, ':encoding(UTF-8)';
    }
}

foreach my $string ( 'Deliver «French Bread»', '日本国' ) {
    my $hashref = { value => $string };
    is_sane_utf8 $string, "String: $string";
    my $json = encode_json($hashref);
    is_sane_utf8 $json, "JSON: $json";
    say STDERR $json;
}
diag ord('»');

done_testing;

这是输出:

utf8.t .. 
ok 1 - String: Deliver «French Bread»
not ok 2 - JSON: {"value":"Deliver «French Bread»"}

#   Failed test 'JSON: {"value":"Deliver «French Bread»"}'
#   at utf8.t line 17.
# Found dodgy chars "<c2><ab>" at char 18
# String not flagged as utf8...was it meant to be?
# Probably originally a LEFT-POINTING DOUBLE ANGLE QUOTATION MARK char - codepoint 171 (dec), ab (hex)
{"value":"Deliver «French Bread»"}    
ok 3 - String: 日本国
ok 4 - JSON: {"value":"æ¥æ¬å½"}
1..4
{"value":"日本国"}
# 187

因此包含guillemets(«»)的字符串是有效的UTF-8,但生成的JSON不是。我错过了什么? utf8编译指示正确标记了我的来源。此外,尾随187来自诊断。这不到255,所以它几乎看起来像Perl中旧的Unicode bug的变种。 (并且测试输出看起来仍然像废话。使用Test :: Builder永远无法做到这一点。

切换到JSON::PP会产生相同的输出。

这是在OS X Yosemite上运行的Perl 5.18.1。

2 个答案:

答案 0 :(得分:13)

is_sane_utf8并不能按照您的想法行事。您可以假设将已解码的字符串传递给它。我不确定它的重点是什么,但它不是正确的工具。如果要检查字符串是否有效UTF-8,可以使用

ok(eval { decode_utf8($string, Encode::FB_CROAK | Encode::LEAVE_SRC); 1 },
   '$string is valid UTF-8');

为了表明JSON :: XS是正确的,让我们看看标记的序列is_sane_utf8

          +--------------------- Start of two byte sequence
          |    +---------------- Not zero (good)     
          |    |     +---------- Continuation byte indicator (good)
          |    |     |
          v    v     v
C2 AB = [110]00010 [10]101011

             00010     101011 = 000 1010 1011 = U+00AB = «

以下显示JSON :: XS生成与Encode.pm相同的输出:

use utf8;
use 5.18.0;
use JSON::XS;
use Encode;

foreach my $string ('Deliver «French Bread»', '日本国') {
    my $hashref = { value => $string };
    say(sprintf("Input: U+%v04X", $string));
    say(sprintf("UTF-8 of input: %v02X", encode_utf8($string)));

    my $json = encode_json($hashref);
    say(sprintf("JSON: %v02X", $json));
    say("");
}

输出(添加了一些空格):

Input: U+0044.0065.006C.0069.0076.0065.0072.0020.00AB.0046.0072.0065.006E.0063.0068.0020.0042.0072.0065.0061.0064.00BB
UTF-8 of input:                     44.65.6C.69.76.65.72.20.C2.AB.46.72.65.6E.63.68.20.42.72.65.61.64.C2.BB
JSON: 7B.22.76.61.6C.75.65.22.3A.22.44.65.6C.69.76.65.72.20.C2.AB.46.72.65.6E.63.68.20.42.72.65.61.64.C2.BB.22.7D

Input: U+65E5.672C.56FD
UTF-8 of input:                     E6.97.A5.E6.9C.AC.E5.9B.BD
JSON: 7B.22.76.61.6C.75.65.22.3A.22.E6.97.A5.E6.9C.AC.E5.9B.BD.22.7D

答案 1 :(得分:4)

JSON :: XS正在生成有效的UTF-8,但您在两个期望字符串的不同上下文中使用生成的UTF-8编码字节字符串。

问题1:测试:: utf8

以下是is_sane_utf8失败时的两种主要情况:

  1. 您有一个错误编码的字符串,该字符串已经从UTF-8字节字符串解码,就像它是Latin-1一样,或者来自双重编码的UTF-8,字符串完全正常看起来像潜在&#34;狡猾&#34;错误编码(使用其文档中的术语)。
  2. 您有一个有效的 UTF-8字节字符串,其中包含编码的代码点U + 0080到U + 00FF,例如«French Bread»
  3. is_sane_utf8测试仅适用于字符串,并且具有记录的漏报可能性。

    问题2:输出编码

    所有非JSON字符串都是字符串,而JSON字符串是UTF-8编码的字节字符串,从JSON编码器返回。由于您正在使用:encoding(UTF-8) PerlIO层进行TAP输出,因此字符串被隐式编码为UTF-8并且结果良好,而包含JSON的字节字符串正在进行双重编码。然而,STDERR没有:encoding PerlIO层集,因此编码的JSON字节字符串在warn中看起来很好,因为它们已经编码并直接传递出来。

    仅对带有字符串的IO使用:encoding(UTF-8) PerlIO层,而不是默认从JSON编码器返回的UTF-8编码字节字符串。