[インデックス 16757] ファイルの概要
このコミットは、Go言語の標準ライブラリ encoding/json
パッケージにおける Marshal
関数(Goのデータ構造をJSON形式に変換する機能)の挙動変更に関するものです。具体的には、入力文字列に無効なUTF-8シーケンスが含まれていた場合に、これまではエラーを返してJSONの生成を中断していましたが、このコミット以降はエラーを返す代わりに、無効なバイトシーケンスをUnicodeの置換文字(U+FFFD, )に置き換えて、有効なUTF-8としてJSONを生成するように変更されました。
コミット
- コミットハッシュ:
64054a40ad0d85e82f77a4982ea4ee08c3cea40a
- Author: Russ Cox rsc@golang.org
- Date: Fri Jul 12 17:37:10 2013 -0400
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/64054a40ad0d85e82f77a4982ea4ee08c3cea40a
元コミット内容
encoding/json: coerce invalid UTF-8 to valid UTF-8 during Marshal
In practice, rejecting an entire structure due to a single invalid byte
in a string is just too picky, and too hard to track down.
Be consistent with the bulk of the standard library by converting
invalid UTF-8 into UTF-8 with replacement runes.
R=golang-dev, crawshaw
CC=golang-dev
https://golang.org/cl/11211045
変更の背景
この変更の主な背景は、実用性(pragmatism)と標準ライブラリ内の一貫性の向上です。
-
実用性の問題: 以前の
encoding/json.Marshal
は、JSONに変換しようとするGoの文字列データ内にたった1バイトでも無効なUTF-8シーケンスが含まれていると、その構造体全体のJSON変換を拒否し、InvalidUTF8Error
を返していました。これは、特に外部システムから受け取ったデータや、ユーザーが入力したデータなど、UTF-8の厳密なバリデーションが難しい状況において、非常に扱いにくい挙動でした。たった一つの無効なバイトが原因で、大規模なデータ構造全体のマーシャリングが失敗し、その原因を特定して修正することが困難になるケースが多発していました。コミットメッセージにある「rejecting an entire structure due to a single invalid byte in a string is just too picky, and too hard to track down」という記述が、この問題意識を端的に表しています。 -
標準ライブラリ内の一貫性: Goの標準ライブラリの他の多くの部分、例えば
io.Reader
やbufio.Scanner
などは、無効なUTF-8シーケンスを検出した場合にエラーを返すのではなく、Unicodeの置換文字(U+FFFD)に置き換えて処理を続行する挙動が一般的でした。encoding/json
パッケージだけが厳密なエラーを返すのは、この一貫性を損なっていました。このコミットは、「Be consistent with the bulk of the standard library by converting invalid UTF-8 into UTF-8 with replacement runes」と述べているように、この一貫性を確保することを目的としています。これにより、開発者はGoの標準ライブラリ全体でUTF-8の扱いに関する予測可能な挙動を期待できるようになります。
これらの理由から、Go 1.2以降、encoding/json.Marshal
は無効なUTF-8を許容し、置換文字に変換する「より寛容な」挙動へと変更されました。
前提知識の解説
このコミットを理解するためには、以下の概念について理解しておく必要があります。
-
UTF-8:
- UTF-8は、Unicode文字をバイトシーケンスにエンコードするための可変長文字エンコーディングです。ASCII文字は1バイトで表現され、他の多くの文字は2バイトから4バイトで表現されます。
- UTF-8は自己同期性(self-synchronizing)を持つように設計されており、バイトストリームの途中で読み取りを開始しても、次の有効な文字の開始位置を比較的容易に特定できます。
- しかし、バイトシーケンスがUTF-8の仕様に準拠していない場合、それは「無効なUTF-8シーケンス」と見なされます。例えば、マルチバイト文字の途中でバイトが欠落したり、不正なバイト値が出現したりする場合です。
-
Unicode置換文字 (U+FFFD):
- U+FFFD () は、Unicodeの「Replacement Character」(置換文字)です。
- これは、テキスト処理において、エンコーディングエラーや、文字セットで表現できない文字が検出された場合に、その不正な文字の代わりに挿入される特殊な文字です。
- この文字が挿入されることで、不正なデータが含まれていても、処理を中断せずに続行できるようになります。
-
JSON (JavaScript Object Notation):
- JSONは、人間が読み書きしやすく、機械が解析しやすいデータ交換フォーマットです。
- JSONの文字列値は、Unicode文字のシーケンスとして定義されており、通常はUTF-8でエンコードされます。JSON仕様では、文字列はUnicode文字のシーケンスであり、特定の制御文字(U+0000からU+001F)はエスケープする必要があります。
-
Goの
encoding/json
パッケージ:- Go言語の標準ライブラリに含まれるパッケージで、Goのデータ構造とJSONデータの間で変換(マーシャリングとアンマーシャリング)を行う機能を提供します。
json.Marshal
関数は、Goの任意の値をJSON形式のバイトスライスに変換します。json.Unmarshal
関数は、JSON形式のバイトスライスをGoのデータ構造に変換します。
-
utf8.DecodeRuneInString
関数:- Goの
unicode/utf8
パッケージに含まれる関数で、文字列の先頭からUTF-8エンコードされたルーン(Unicodeコードポイント)をデコードします。 - この関数は、デコードされたルーンと、そのルーンを構成するバイト数(サイズ)を返します。
- もし入力が有効なUTF-8シーケンスでなかった場合、
utf8.RuneError
(U+FFFD) と、その不正なシーケンスを構成するバイト数(通常は1)を返します。このutf8.RuneError
とsize == 1
の組み合わせは、単一の不正なバイトが検出されたことを示します。
- Goの
技術的詳細
このコミットが行われる前、encoding/json.Marshal
は、Goの文字列をJSON文字列としてエンコードする際に、その文字列が厳密に有効なUTF-8であるかを検査していました。もし文字列内に無効なUTF-8バイトシーケンス(例えば、\xff
のような単一の不正なバイト)が検出された場合、Marshal
は *json.InvalidUTF8Error
型のエラーを即座に返し、JSONの生成を中断していました。これは、データソースが完全にUTF-8準拠でない場合に、アプリケーションがJSONを生成できないという問題を引き起こしていました。
このコミットによって、Marshal
の挙動は以下のように変更されました。
- エラーの廃止:
InvalidUTF8Error
は、もはやMarshal
によって生成されなくなりました。ただし、後方互換性のために型定義自体は残されています。これは、既存のコードがこのエラー型を参照している場合にコンパイルエラーにならないようにするためです。 - 置換文字への変換:
Marshal
は、文字列を走査する際にutf8.DecodeRuneInString
を使用して各ルーンをデコードします。- もし
utf8.DecodeRuneInString
がutf8.RuneError
とsize == 1
を返した場合(これは単一の不正なバイトが検出されたことを意味します)、Marshal
はその不正なバイトをJSON文字列内で\ufffd
(Unicode置換文字U+FFFDのエスケープシーケンス) に置き換えます。 - これにより、出力されるJSON文字列は常に有効なUTF-8となり、マーシャリング処理全体が中断されることなく完了します。
- もし
この変更は、doc/go1.2.txt
にも「encoding/json
: accept but correct invalid UTF-8 in Marshal (CL 11211045)」として記載されており、Go 1.2のリリースノートの一部として公式にアナウンスされました。
テストケース TestMarshalBadUTF8
の変更もこの挙動を明確に示しています。以前は Marshal("hello\xffworld")
がエラーを返すことを期待していましたが、変更後はエラーを返さず、"hello\ufffdworld"
というJSON文字列が生成されることを期待するように修正されています。
コアとなるコードの変更箇所
変更は主に src/pkg/encoding/json/encode.go
ファイルの encodeState.string
メソッド内で行われました。
--- a/src/pkg/encoding/json/encode.go
+++ b/src/pkg/encoding/json/encode.go
@@ -209,8 +209,12 @@ func (e *UnsupportedValueError) Error() string {
return "json: unsupported value: " + e.Str
}
-// An InvalidUTF8Error is returned by Marshal when attempting
-// to encode a string value with invalid UTF-8 sequences.
+// Before Go 1.2, an InvalidUTF8Error was returned by Marshal when
+// attempting to encode a string value with invalid UTF-8 sequences.
+// As of Go 1.2, Marshal instead coerces the string to valid UTF-8 by
+// replacing invalid bytes with the Unicode replacement rune U+FFFD.
+// This error is no longer generated but is kept for backwards compatibility
+// with programs that might mention it.
type InvalidUTF8Error struct {
S string // the whole string value that caused the error
}
@@ -555,7 +559,13 @@ func (e *encodeState) string(s string) (int, error) {
c, size := utf8.DecodeRuneInString(s[i:])
if c == utf8.RuneError && size == 1 {
// This is an invalid UTF-8 byte.
- e.error(&InvalidUTF8Error{s})
+ // Replace it with the Unicode replacement character.
+ if start < i {
+ e.WriteString(s[start:i])
+ }
+ e.WriteString(`\ufffd`)
+ i += size
+ start = i
+ continue
}
// U+2028 is LINE SEPARATOR.
// U+2029 is PARAGRAPH SEPARATOR.
コアとなるコードの解説
encodeState.string
メソッドは、Goの文字列をJSON文字列としてエンコードする際の主要なロジックを含んでいます。このメソッドは、入力文字列 s
をバイトごとに走査し、JSONの仕様に従ってエスケープ処理などを行います。
変更された部分の核心は、if c == utf8.RuneError && size == 1
の条件ブロックです。
utf8.DecodeRuneInString(s[i:])
は、現在の位置i
から始まる文字列のサブスライスからUTF-8ルーンをデコードしようとします。c == utf8.RuneError
は、デコードされたルーンがutf8.RuneError
(U+FFFD) であることを意味します。これは、入力バイトシーケンスが有効なUTF-8ルーンとして解釈できなかった場合に返されます。size == 1
は、utf8.RuneError
が返された原因が、単一の不正なバイトであったことを示します。例えば、\xff
のようなバイトは、それ単独では有効なUTF-8文字を構成できません。
変更前:
この条件が真であった場合、e.error(&InvalidUTF8Error{s})
が呼び出され、Marshal
処理全体が InvalidUTF8Error
を伴って中断されていました。
変更後: エラーを返す代わりに、以下の処理が行われます。
if start < i { e.WriteString(s[start:i]) }
:start
は最後に有効な文字が書き込まれた位置、i
は現在の不正なバイトの位置です。この条件が真の場合、start
からi
までの有効な文字列部分をまず出力バッファに書き込みます。これにより、不正なバイトの手前までの文字列が正しく処理されます。e.WriteString(
\ufffd)
: ここが最も重要な変更点です。不正なUTF-8バイトシーケンスの代わりに、Unicode置換文字U+FFFDのJSONエスケープシーケンスである\ufffd
を出力バッファに書き込みます。i += size
:i
をsize
(この場合は1) だけ進めます。これにより、不正なバイトがスキップされ、次の文字の処理に進みます。start = i
:start
を現在のi
の位置に更新します。これは、次の有効な文字列部分の開始点となります。continue
: ループの次のイテレーションに進み、文字列の残りの部分の処理を続行します。
この変更により、Marshal
は無効なUTF-8バイトを検出しても処理を中断せず、代わりに \ufffd
を挿入することで、常に有効なUTF-8を含むJSON出力を生成するようになりました。
関連リンク
- Go CL 11211045: https://golang.org/cl/11211045
- Go 1.2 Release Notes: https://go.dev/doc/go1.2 (特に "encoding/json" セクション)
参考にした情報源リンク
- UTF-8 - Wikipedia
- Unicode置換文字 - Wikipedia
- JSON - Wikipedia
- Go言語のencoding/jsonパッケージのドキュメント
- Go言語のunicode/utf8パッケージのドキュメント
- Go 1.2 Release Notes (特に
encoding/json
の変更点) - Go issue 5706: encoding/json: Marshal should coerce invalid UTF-8 to valid UTF-8 (このコミットに関連するGitHub Issue)
[インデックス 16757] ファイルの概要
このコミットは、Go言語の標準ライブラリ encoding/json
パッケージにおける Marshal
関数(Goのデータ構造をJSON形式に変換する機能)の挙動変更に関するものです。具体的には、入力文字列に無効なUTF-8シーケンスが含まれていた場合に、これまではエラーを返してJSONの生成を中断していましたが、このコミット以降はエラーを返す代わりに、無効なバイトシーケンスをUnicodeの置換文字(U+FFFD, )に置き換えて、有効なUTF-8としてJSONを生成するように変更されました。
コミット
- コミットハッシュ:
64054a40ad0d85e82f77a4982ea4ee08c3cea40a
- Author: Russ Cox rsc@golang.org
- Date: Fri Jul 12 17:37:10 2013 -0400
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/64054a40ad0d85e82f77a4982ea4ee08c3cea40a
元コミット内容
encoding/json: coerce invalid UTF-8 to valid UTF-8 during Marshal
In practice, rejecting an entire structure due to a single invalid byte
in a string is just too picky, and too hard to track down.
Be consistent with the bulk of the standard library by converting
invalid UTF-8 into UTF-8 with replacement runes.
R=golang-dev, crawshaw
CC=golang-dev
https://golang.org/cl/11211045
変更の背景
この変更の主な背景は、実用性(pragmatism)と標準ライブラリ内の一貫性の向上です。
-
実用性の問題: 以前の
encoding/json.Marshal
は、JSONに変換しようとするGoの文字列データ内にたった1バイトでも無効なUTF-8シーケンスが含まれていると、その構造体全体のJSON変換を拒否し、InvalidUTF8Error
を返していました。これは、特に外部システムから受け取ったデータや、ユーザーが入力したデータなど、UTF-8の厳密なバリデーションが難しい状況において、非常に扱いにくい挙動でした。たった一つの無効なバイトが原因で、大規模なデータ構造全体のマーシャリングが失敗し、その原因を特定して修正することが困難になるケースが多発していました。コミットメッセージにある「rejecting an entire structure due to a single invalid byte in a string is just too picky, and too hard to track down」という記述が、この問題意識を端的に表しています。 -
標準ライブラリ内の一貫性: Goの標準ライブラリの他の多くの部分、例えば
io.Reader
やbufio.Scanner
などは、無効なUTF-8シーケンスを検出した場合にエラーを返すのではなく、Unicodeの置換文字(U+FFFD)に置き換えて処理を続行する挙動が一般的でした。encoding/json
パッケージだけが厳密なエラーを返すのは、この一貫性を損なっていました。このコミットは、「Be consistent with the bulk of the standard library by converting invalid UTF-8 into UTF-8 with replacement runes」と述べているように、この一貫性を確保することを目的としています。これにより、開発者はGoの標準ライブラリ全体でUTF-8の扱いに関する予測可能な挙動を期待できるようになります。
これらの理由から、Go 1.2以降、encoding/json.Marshal
は無効なUTF-8を許容し、置換文字に変換する「より寛容な」挙動へと変更されました。
前提知識の解説
このコミットを理解するためには、以下の概念について理解しておく必要があります。
-
UTF-8:
- UTF-8は、Unicode文字をバイトシーケンスにエンコードするための可変長文字エンコーディングです。ASCII文字は1バイトで表現され、他の多くの文字は2バイトから4バイトで表現されます。
- UTF-8は自己同期性(self-synchronizing)を持つように設計されており、バイトストリームの途中で読み取りを開始しても、次の有効な文字の開始位置を比較的容易に特定できます。
- しかし、バイトシーケンスがUTF-8の仕様に準拠していない場合、それは「無効なUTF-8シーケンス」と見なされます。例えば、マルチバイト文字の途中でバイトが欠落したり、不正なバイト値が出現したりする場合です。
-
Unicode置換文字 (U+FFFD):
- U+FFFD () は、Unicodeの「Replacement Character」(置換文字)です。
- これは、テキスト処理において、エンコーディングエラーや、文字セットで表現できない文字が検出された場合に、その不正な文字の代わりに挿入される特殊な文字です。
- この文字が挿入されることで、不正なデータが含まれていても、処理を中断せずに続行できるようになります。
-
JSON (JavaScript Object Notation):
- JSONは、人間が読み書きしやすく、機械が解析しやすいデータ交換フォーマットです。
- JSONの文字列値は、Unicode文字のシーケンスとして定義されており、通常はUTF-8でエンコードされます。JSON仕様では、文字列はUnicode文字のシーケンスであり、特定の制御文字(U+0000からU+001F)はエスケープする必要があります。
-
Goの
encoding/json
パッケージ:- Go言語の標準ライブラリに含まれるパッケージで、Goのデータ構造とJSONデータの間で変換(マーシャリングとアンマーシャリング)を行う機能を提供します。
json.Marshal
関数は、Goの任意の値をJSON形式のバイトスライスに変換します。json.Unmarshal
関数は、JSON形式のバイトスライスをGoのデータ構造に変換します。
-
utf8.DecodeRuneInString
関数:- Goの
unicode/utf8
パッケージに含まれる関数で、文字列の先頭からUTF-8エンコードされたルーン(Unicodeコードポイント)をデコードします。 - この関数は、デコードされたルーンと、そのルーンを構成するバイト数(サイズ)を返します。
- もし入力が有効なUTF-8シーケンスでなかった場合、
utf8.RuneError
(U+FFFD) と、その不正なシーケンスを構成するバイト数(通常は1)を返します。このutf8.RuneError
とsize == 1
の組み合わせは、単一の不正なバイトが検出されたことを示します。
- Goの
技術的詳細
このコミットが行われる前、encoding/json.Marshal
は、Goの文字列をJSON文字列としてエンコードする際に、その文字列が厳密に有効なUTF-8であるかを検査していました。もし文字列内に無効なUTF-8バイトシーケンス(例えば、\xff
のような単一の不正なバイト)が検出された場合、Marshal
は *json.InvalidUTF8Error
型のエラーを即座に返し、JSONの生成を中断していました。これは、データソースが完全にUTF-8準拠でない場合に、アプリケーションがJSONを生成できないという問題を引き起こしていました。
このコミットによって、Marshal
の挙動は以下のように変更されました。
- エラーの廃止:
InvalidUTF8Error
は、もはやMarshal
によって生成されなくなりました。ただし、後方互換性のために型定義自体は残されています。これは、既存のコードがこのエラー型を参照している場合にコンパイルエラーにならないようにするためです。 - 置換文字への変換:
Marshal
は、文字列を走査する際にutf8.DecodeRuneInString
を使用して各ルーンをデコードします。- もし
utf8.DecodeRuneInString
がutf8.RuneError
とsize == 1
を返した場合(これは単一の不正なバイトが検出されたことを意味します)、Marshal
はその不正なバイトをJSON文字列内で\ufffd
(Unicode置換文字U+FFFDのエスケープシーケンス) に置き換えます。 - これにより、出力されるJSON文字列は常に有効なUTF-8となり、マーシャリング処理全体が中断されることなく完了します。
- もし
この変更は、doc/go1.2.txt
にも「encoding/json
: accept but correct invalid UTF-8 in Marshal (CL 11211045)」として記載されており、Go 1.2のリリースノートの一部として公式にアナウンスされました。
テストケース TestMarshalBadUTF8
の変更もこの挙動を明確に示しています。以前は Marshal("hello\xffworld")
がエラーを返すことを期待していましたが、変更後はエラーを返さず、"hello\ufffdworld"
というJSON文字列が生成されることを期待するように修正されています。
コアとなるコードの変更箇所
変更は主に src/pkg/encoding/json/encode.go
ファイルの encodeState.string
メソッド内で行われました。
--- a/src/pkg/encoding/json/encode.go
+++ b/src/pkg/encoding/json/encode.go
@@ -209,8 +209,12 @@ func (e *UnsupportedValueError) Error() string {
return "json: unsupported value: " + e.Str
}
-// An InvalidUTF8Error is returned by Marshal when attempting
-// to encode a string value with invalid UTF-8 sequences.
+// Before Go 1.2, an InvalidUTF8Error was returned by Marshal when
+// attempting to encode a string value with invalid UTF-8 sequences.
+// As of Go 1.2, Marshal instead coerces the string to valid UTF-8 by
+// replacing invalid bytes with the Unicode replacement rune U+FFFD.
+// This error is no longer generated but is kept for backwards compatibility
+// with programs that might mention it.
type InvalidUTF8Error struct {
S string // the whole string value that caused the error
}
@@ -555,7 +559,13 @@ func (e *encodeState) string(s string) (int, error) {
c, size := utf8.DecodeRuneInString(s[i:])
if c == utf8.RuneError && size == 1 {
// This is an invalid UTF-8 byte.
- e.error(&InvalidUTF8Error{s})
+ // Replace it with the Unicode replacement character.
+ if start < i {
+ e.WriteString(s[start:i])
+ }
+ e.WriteString(`\ufffd`)
+ i += size
+ start = i
+ continue
}
// U+2028 is LINE SEPARATOR.
// U+2029 is PARAGRAPH SEPARATOR.
コアとなるコードの解説
encodeState.string
メソッドは、Goの文字列をJSON文字列としてエンコードする際の主要なロジックを含んでいます。このメソッドは、入力文字列 s
をバイトごとに走査し、JSONの仕様に従ってエスケープ処理などを行います。
変更された部分の核心は、if c == utf8.RuneError && size == 1
の条件ブロックです。
utf8.DecodeRuneInString(s[i:])
は、現在の位置i
から始まる文字列のサブスライスからUTF-8ルーンをデコードしようとします。c == utf8.RuneError
は、デコードされたルーンがutf8.RuneError
(U+FFFD) であることを意味します。これは、入力バイトシーケンスが有効なUTF-8ルーンとして解釈できなかった場合に返されます。size == 1
は、utf8.RuneError
が返された原因が、単一の不正なバイトであったことを示します。例えば、\xff
のようなバイトは、それ単独では有効なUTF-8文字を構成できません。
変更前:
この条件が真であった場合、e.error(&InvalidUTF8Error{s})
が呼び出され、Marshal
処理全体が InvalidUTF8Error
を伴って中断されていました。
変更後: エラーを返す代わりに、以下の処理が行われます。
if start < i { e.WriteString(s[start:i]) }
:start
は最後に有効な文字が書き込まれた位置、i
は現在の不正なバイトの位置です。この条件が真の場合、start
からi
までの有効な文字列部分をまず出力バッファに書き込みます。これにより、不正なバイトの手前までの文字列が正しく処理されます。e.WriteString(
\ufffd)
: ここが最も重要な変更点です。不正なUTF-8バイトシーケンスの代わりに、Unicode置換文字U+FFFDのJSONエスケープシーケンスである\ufffd
を出力バッファに書き込みます。i += size
:i
をsize
(この場合は1) だけ進めます。これにより、不正なバイトがスキップされ、次の文字の処理に進みます。start = i
:start
を現在のi
の位置に更新します。これは、次の有効な文字列部分の開始点となります。continue
: ループの次のイテレーションに進み、文字列の残りの部分の処理を続行します。
この変更により、Marshal
は無効なUTF-8バイトを検出しても処理を中断せず、代わりに \ufffd
を挿入することで、常に有効なUTF-8を含むJSON出力を生成するようになりました。
関連リンク
- Go CL 11211045: https://golang.org/cl/11211045
- Go 1.2 Release Notes: https://go.dev/doc/go1.2 (特に "encoding/json" セクション)
参考にした情報源リンク
- UTF-8 - Wikipedia
- Unicode置換文字 - Wikipedia
- JSON - Wikipedia
- Go言語のencoding/jsonパッケージのドキュメント
- Go言語のunicode/utf8パッケージのドキュメント
- Go 1.2 Release Notes (特に
encoding/json
の変更点) - Go issue 5706: encoding/json: Marshal should coerce invalid UTF-8 to valid UTF-8 (このコミットに関連するGitHub Issue)