rurema / doctree

Repository of Japanese Ruby reference manual
https://docs.ruby-lang.org/ja/
242 stars 312 forks source link

String#to_json の説明 #1651

Open scivola opened 5 years ago

scivola commented 5 years ago

JSON::Generator::GeneratorMethods::String#to_json の説明に

"\u????" のように UTF-16 ビッグエンディアンでエンコードされた文字列を返すことがあります。

とありますが,\uXXXX の表記はエンディアンに関係がないので「UTF-16 ビッグエンディアンで」は何かの間違いだと思います。 関連 #1646

scivola commented 5 years ago

もう一つの問題点を書き忘れました。

1646 のほうに書いたのですが,RFC 8259 によれば,\u の表記は 16 進 4 桁であり,BMP(基本多言語面)外の文字の場合は UTF-16 サロゲートペアを使って \uD834\uDD1E のように表現することになっているようです。

参考:RFC 8259 — The JavaScript Object Notation (JSON) Data Interchange Format の「7. Strings」

しかるに,"\u{1D11E}".to_json をやってみると,\u 表記になる場合は \uD834\uDD1E ではなく \u{1D11E} になります。

なぜ RFC 8259 に従っていないのかは分からないですが,従わない理由を書くか,それが無理でも,注意を喚起する必要があると思います。

sho-h commented 5 years ago

とありますが,\uXXXX の表記はエンディアンに関係がないので「UTF-16 ビッグエンディアンで」は何かの間違いだと思います。

とりあえずここをもう少し詳しく書いていただけるとありがたいです。本体に含まれてないですが、以下のあたりの話かもしれませんね。とだけ思いました。

https://github.com/flori/json/blob/b3ec252120f4a5c12de3ffcf16b2540bdea79248/lib/json/pure/generator.rb#L2-L48

scivola commented 5 years ago

まず,「RFC 8259 に従っていない」は事実無根でした。すみません。BMP 外の文字の \u 表記について,RFC 8259 に従っていることが確認できました。#1646 のサンプルコードの結果を読み違えていたために誤解してしまいました。 これについては #1646 のほうで改めて訂正コメントを書きます。

さて

ここをもう少し詳しく書いていただけると

につきまして。 UTF-16 BE(ビッグエンディアン)は,UTF-16 の 符号単位(16 bit)を上位 8 bit(1 byte)と下位 8 bit に分け,上位→下位の順にバイトを並べるエンコーディング方式ですね。

一方,\u 表記は,バイトの並びではないので(コードポイントを 4 桁の 16 進数で表して \u を頭につけたものなので),エンディアンという概念と無関係です。

ただ utf8_to_json メソッドのコメントに「UTF16 big endian characters」と書かれた理由は想像がつきます。 U+3042(あ)だったら,\uXXXX の表記は「\u3042」になり,UTF-16 BE のバイト列を 16 進数表示すれば「30 42」となって,見た目が似ているからです。 また,utf8_to_json_ascii メソッドにおいて,\u 表記を作るためにいったん UTF-16 BE にエンコードしていることも関係ありそうです。

なお,当該の コメント

# Convert a UTF8 encoded Ruby string string to a JSON string, encoded with # UTF16 big endian characters as \u????, and return it.

は,直後の utf8_to_json メソッドと,その次の utf8_to_json_ascii の両方の説明を兼ねているように思われました。

どんな場合に \u 表記になるかは,JSON::State に依存し,

ということであるようです。

scivola commented 5 years ago

詳しく調べていませんが,String#to_json は引数を取るようですので,そのあたりも修正が必要ですね。

sho-h commented 5 years ago

ありがとうございます。

UTF-16 BE(ビッグエンディアン)は,UTF-16 の 符号単位(16 bit)を上位 8 bit(1 byte)と下位 8 bit に分け,上位→下位の順にバイトを並べるエンコーディング方式ですね。

一方,\u 表記は,バイトの並びではないので(コードポイントを 4 桁の 16 進数で表して \u を頭につけたものなので),エンディアンという概念と無関係です。

\u 自体の表現の仕方の範囲ではエンディアンとは無関係というところまではわかったような気がします。

一方で、RFC 8259 のサロゲートペアのサンプルに言及いただいてますが、以下を試す範囲では Encoding::UTF_16BE な文字を返してるのではないのか?とも思います。

puts "\u{1D11E}".unpack("C*").map {|n| n.to_s(16) }.join
f09d849e

puts "\u{1D11E}".encode("utf-16be").unpack("C*").map {|n| n.to_s(16) }.join
d834dd1e

puts "\u{1D11E}".encode("utf-16").unpack("C*").map {|n| n.to_s(16) }.join
feffd834dd1e

puts "\u{1D11E}".encode("utf-16le").unpack("C*").map {|n| n.to_s(16) }.join
34d81edd

puts "\u{1D11E}".to_json(ascii_only: true)
"\ud834\udd1e"

puts JSON.parse("\"\\ud834\\udd1e\"").unpack("C*").map {|n| n.to_s(16) }.join
f09d849e

ところで、\u 表記を使った時の文字エンコードをどう識別するのかわかってないのですが、\u 表記で UTF-8 も UTF-16 BE も表す事ができるとすれば\u 表記を行ってエスケープした文字列を使ったJSONは UTF-8 で記述する仕様から外れていないのではないか?とも思ったりします。ただ、これが的を得ているのかがよくわかりませんでした。

参照いただいてるのだと思いますが、 RFC 8259 の 7. Strings を見ますと以下の辺りで件の文字も出てきます。

To escape an extended character that is not in the Basic Multilingual Plane, the character is represented as a 12-character sequence, encoding the UTF-16 surrogate pair.  So, for example, a string containing only the G clef character (U+1D11E) may be represented as "\uD834\uDD1E".

これを読む分には、

のではないかと思いました。

ただし、「"\u????" のように」と説明を始めてしまうと \u 表記全般が「UTF-16 BEな文字列である」と読めてしまうかもしれないとは思いましたので、説明をもう少し考えてもいいのではないかと思いました。

scivola commented 5 years ago

以下を試す範囲では Encoding::UTF_16BE な文字を返してるのではないのか?とも思います。

の意味がちょっとよく分かりませんでした。

RFC 8259 が求めているのは,文字を \u 表記にするとき,

ということだと思います(実際 Ruby の json はそのように動作しています)。 逆にデコード(JSON 文字列のパース)の場合,\u 表記が出てきたら,

と。

サロゲートペアは UTF-16 のために考案されたもので,それを用いて \u 表記を構成する,という点では UTF-16 と関係のある話ですが,それ以上ではないと思います。 String#to_json メソッドは UTF-16 BE へのエンコード変換を内部で使っていますが,これは実装の都合(簡単にサロゲートペア化できる;結果をバイト順に 16 進表記してつなげれば \u 表記の 16 進文字列が得られる)であって,ユーザーには関係のない話だと思います。

RFC 8259 に「UTF-16」は 3 箇所に出てきます。 一つは,引用していただいた

encoding the UTF-16 surrogate pair

のところ。(この「encoding」はサロゲートペア化のことと思われます) あとの二つは,「もし JSON 文字列にサロゲートペアの片割れが単独で出てきたらどうすんだよ(超訳)」という話のところです。 UTF-16 BE については言及がありません。

sho-h commented 5 years ago

String#to_json メソッドは UTF-16 BE へのエンコード変換を内部で使っていますが,これは実装の都合(簡単にサロゲートペア化できる;結果をバイト順に 16 進表記してつなげれば \u 表記の 16 進文字列が得られる)であって,ユーザーには関係のない話だと思います。

すみません、ここがよくわかりませんでした。「内部で使っていますが」とありますが、今のところその結果をそのまま返してると思っています。上で挙げた以下のコードの部分です。

puts "\u{1D11E}".encode("utf-16be").unpack("C*").map {|n| n.to_s(16) }.join
d834dd1e

puts "\u{1D11E}".to_json(ascii_only: true)
"\ud834\udd1e"

ちなみにですが、どう修正して欲しいのでしょうか? @scivola さんの主張が正しい場合、最も詳しいと思いますので、修正内容もわかるのではないかと思います。以下の2点を整理していただけるとありがたいですね。

sho-h commented 5 years ago

(細かい仕組みよりも「戻り値がどういう値」だからエンディアンが関係ないという話をしていただく方が個人的にうれしいですね。

scivola commented 5 years ago

すれ違いの原因が判ったような気がします。

掲げていただいた

puts "\u{1D11E}".to_json(ascii_only: true)
# => "\ud834\udd1e"

ですが,p ではなく puts であるところがポイントですね。

↓こちらのほうが分かりやすいかもしれません。

p "\u{1D11E}".to_json(ascii_only: true).chars
# => ["\"", "\\", "u", "d", "8", "3", "4", "\\", "u", "d", "d", "1", "e", "\""]

この to_json メソッドは,BMP 外な文字一つだけの文字列に対して呼び出されて,その文字をサロゲートペアにしてそれぞれの \u 表記を作り,それらをつなげたうえで全体を " " で囲んだ String オブジェクトを返しています。 この返り値は ASCII 文字だけで構成されています。どこにも UTF-16 BE は出てきません。

と,こういう話で納得していただけそうでしょうか。

どう修正して欲しいのでしょうか?

修正提案が書けなかったのは,私自身,よく分かってなかったからなのですが,だんだん分かってきました。 まだちょっとモヤモヤしたところがあるのですが,utf8_to_json_ascii メソッドらのやっていることがもう少し理解できたら修正案を考えてみます。

sho-h commented 5 years ago

うーん、残念ながらわかってないです。

この返り値は ASCII 文字だけで構成されています。

はい。(前コメントのお話とは関係なく)その通りだと思っています。

どこにも UTF-16 BE は出てきません。

"\ud834\udd1e" が表す文字のエンコーディングがUTF-16BEなのだという主張をコード例で示していたつもりでした。素性がよくわかってないサイト(僕が知らないだけですけども)ですが、以下などでもそう見えます。

UTF-16 BE については言及がありません。

と書かれていますけども、別の例ではUTF-16LE、UTF-16でencodeして異なる値が出てますので、やはりUTF-16 BEなのではないかという認識ですね。サロゲートペアのお話よりも、まずこの辺りを否定してもらえるのが一番ありがたいというところですね。

scivola commented 5 years ago

すると,to_json の挙動についての認識はたぶん一致していますね。

そして,問題の焦点はサロゲートペアにはなさそうですので,三日前のコメント に戻っちゃいますが,「あ」で考えたいと思います。

「あ」を UTF-16 BE で表すと,第 1 バイトが 0x30,第 2 バイトが 0x42 になります。 この 16 進数を数字だけにしてくっつけて書くと 3042 ですね。

それで,

"あ".to_json(ascii_only: true)

が返す "\u3042" という文字列は UTF-16 BE ではないか? ということを仰っているのでしょうか?

UTF-16 BE は,コードポイントの数値を上位 8 bit →下位 8 bit の順に並べた物なので,そのバイト列を 16 進表記すれば,当然,コードポイントの数値を 4 桁の 16 進数で表したものと一致します。 したがって,\u 表記に用いる長さ 4 の文字列とも一致します。

しかし,そのことをもって \u 表記は UTF-16 BE だ,とは言えないでしょう。 UTF-16 BE はバイト列ですし,\u は ASCII 文字で Unicode の文字を表記したものですので。

sho-h commented 5 years ago

しかし,そのことをもって \u 表記は UTF-16 BE だ,とは言えないでしょう。

いえ、そうとは一度も言ってないです。再掲しますが僕の見解は引き続き以下ですね。

改めてですが、問題にされている文言は以下ですよね。

"\u????" のように UTF-16 ビッグエンディアンでエンコードされた文字列を返すことがあります。

「ことがある」という文言に「UTF-16BEの文字を表す\u表記が登場し得る」という解釈をしています。この時に以下のようにコメントしています。

ただし、「"\u????" のように」と説明を始めてしまうと \u 表記全般が「UTF-16 BEな文字列である」と読めてしまうかもしれないとは思いましたので、説明をもう少し考えてもいいのではないかと思いました。

「\u 表記とUTF-16BEを切り離す」という修正は最低限必要だとは思っていますという事ですね。

ただ、たとえば「ascii_onlyオプションを指定した場合に、UTF-16ビッグエンディアンでエンコードされた文字列を表す \u???? 形式の文字列を返します」と修正すればOKなのか、それでは足りないのかはわかっていません。(...切り離せていない気がしますけど、それは一旦おいておきます)

しかし,そのことをもって \u 表記は UTF-16 BE だ,とは言えないでしょう。 UTF-16 BE はバイト列ですし,\u は ASCII 文字で Unicode の文字を表記したものですので。

たまたま別の文字エンコーディングでも同じ値を示す文字の事を話しているのでは?という事であればその通りだと思います。

ただ、(解釈があっていれば)それは流石に具体的に反証をコードで例示していただくとありがたいです。テストコードとか見ていただくといいかもしれませんね。

sho-h commented 5 years ago

ところでjsonライブラリとしての\u表記の定義というものがあるかないかが気になっていたのですが、少し探しましたけどなさそうですね。

それが存在しないようなら一般的なものを見たらいいかとおもうのですが、そこに「U+3042を\u3042と表記します」のような記載があるなら、「それでは足りないのかはわかっていません」の部分は明らかに足りませんと考えると思います。

もしそうなら、修正内容は割と単純だったりするのだろうか?とも思ったりするのですが...

scivola commented 5 years ago

用語の意味についての認識が一部食い違っているのかも,という気がしています。

「UTF-16 BEな文字列を返す」事はある 「UTF-16BEの文字を表す\u表記が登場し得る」

における「UTF-16 BE な文字列」「UTF-16 BE の文字」が何を意味するのか私にはよく分からないです。

UTF-16 BE は符号化方式(バイト列を構成する方式)の名前なので,「UTF-16 BE な文字列」という表現を見たら,私は「UTF-16 BE にしたがって作られたバイト列なんだな」と考えます。 しかし,String#to_json の返り値は UTF-8 なので,この中に UTF-16 BE のバイト列が混在することはありません。したがって「そういう意味で書かれたのではないのだろう」とは思うのですが,ではどういう意味なのかが分からないんです。

それから

jsonライブラリとしての\u表記の定義

ですが,「ライブラリーとしての定義」というのがよく分かりませんでした。 JSON というフォーマットにおける \u 表記の定義は RFC 8259 の「7. Strings」に書かれているのがそれですね。 つまり,文字列の表現において

(英語苦手なので 100% の自信は無いのですが)

json ライブラリーではこれをそのまま実装しているように思われます。 ascii_only: false の場合はエスケープ必須の文字だけをエスケープし,ascii_only: true の場合はそれに加えて非 ASCII 文字もエスケープする,と。 (これも,コードをまだじっくり読んでないので,100% の自信は無いのですが。 あとで読もうと思いながら時間が取れないでいます。すいません。)

sho-h commented 5 years ago

やっとわかって来た気がします。あとでもう一度読もうと思いますが、やはりどう直せばいいのか、 @scivola さんご存知なのでは?

scivola commented 5 years ago

はい,修正の下案を作ってみようと思っているのですが,その前に utf8_to_json_ascii などの実装をよく読んでからと思っていて,なかなか取り掛かれなくてすみません。