Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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)と標準ライブラリ内の一貫性の向上です。

  1. 実用性の問題: 以前の 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」という記述が、この問題意識を端的に表しています。

  2. 標準ライブラリ内の一貫性: Goの標準ライブラリの他の多くの部分、例えば io.Readerbufio.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を許容し、置換文字に変換する「より寛容な」挙動へと変更されました。

前提知識の解説

このコミットを理解するためには、以下の概念について理解しておく必要があります。

  1. UTF-8:

    • UTF-8は、Unicode文字をバイトシーケンスにエンコードするための可変長文字エンコーディングです。ASCII文字は1バイトで表現され、他の多くの文字は2バイトから4バイトで表現されます。
    • UTF-8は自己同期性(self-synchronizing)を持つように設計されており、バイトストリームの途中で読み取りを開始しても、次の有効な文字の開始位置を比較的容易に特定できます。
    • しかし、バイトシーケンスがUTF-8の仕様に準拠していない場合、それは「無効なUTF-8シーケンス」と見なされます。例えば、マルチバイト文字の途中でバイトが欠落したり、不正なバイト値が出現したりする場合です。
  2. Unicode置換文字 (U+FFFD):

    • U+FFFD () は、Unicodeの「Replacement Character」(置換文字)です。
    • これは、テキスト処理において、エンコーディングエラーや、文字セットで表現できない文字が検出された場合に、その不正な文字の代わりに挿入される特殊な文字です。
    • この文字が挿入されることで、不正なデータが含まれていても、処理を中断せずに続行できるようになります。
  3. JSON (JavaScript Object Notation):

    • JSONは、人間が読み書きしやすく、機械が解析しやすいデータ交換フォーマットです。
    • JSONの文字列値は、Unicode文字のシーケンスとして定義されており、通常はUTF-8でエンコードされます。JSON仕様では、文字列はUnicode文字のシーケンスであり、特定の制御文字(U+0000からU+001F)はエスケープする必要があります。
  4. Goの encoding/json パッケージ:

    • Go言語の標準ライブラリに含まれるパッケージで、Goのデータ構造とJSONデータの間で変換(マーシャリングとアンマーシャリング)を行う機能を提供します。
    • json.Marshal 関数は、Goの任意の値をJSON形式のバイトスライスに変換します。
    • json.Unmarshal 関数は、JSON形式のバイトスライスをGoのデータ構造に変換します。
  5. utf8.DecodeRuneInString 関数:

    • Goの unicode/utf8 パッケージに含まれる関数で、文字列の先頭からUTF-8エンコードされたルーン(Unicodeコードポイント)をデコードします。
    • この関数は、デコードされたルーンと、そのルーンを構成するバイト数(サイズ)を返します。
    • もし入力が有効なUTF-8シーケンスでなかった場合、utf8.RuneError (U+FFFD) と、その不正なシーケンスを構成するバイト数(通常は1)を返します。この utf8.RuneErrorsize == 1 の組み合わせは、単一の不正なバイトが検出されたことを示します。

技術的詳細

このコミットが行われる前、encoding/json.Marshal は、Goの文字列をJSON文字列としてエンコードする際に、その文字列が厳密に有効なUTF-8であるかを検査していました。もし文字列内に無効なUTF-8バイトシーケンス(例えば、\xff のような単一の不正なバイト)が検出された場合、Marshal*json.InvalidUTF8Error 型のエラーを即座に返し、JSONの生成を中断していました。これは、データソースが完全にUTF-8準拠でない場合に、アプリケーションがJSONを生成できないという問題を引き起こしていました。

このコミットによって、Marshal の挙動は以下のように変更されました。

  1. エラーの廃止: InvalidUTF8Error は、もはや Marshal によって生成されなくなりました。ただし、後方互換性のために型定義自体は残されています。これは、既存のコードがこのエラー型を参照している場合にコンパイルエラーにならないようにするためです。
  2. 置換文字への変換: Marshal は、文字列を走査する際に utf8.DecodeRuneInString を使用して各ルーンをデコードします。
    • もし utf8.DecodeRuneInStringutf8.RuneErrorsize == 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 を伴って中断されていました。

変更後: エラーを返す代わりに、以下の処理が行われます。

  1. if start < i { e.WriteString(s[start:i]) }: start は最後に有効な文字が書き込まれた位置、i は現在の不正なバイトの位置です。この条件が真の場合、start から i までの有効な文字列部分をまず出力バッファに書き込みます。これにより、不正なバイトの手前までの文字列が正しく処理されます。
  2. e.WriteString(\ufffd): ここが最も重要な変更点です。不正なUTF-8バイトシーケンスの代わりに、Unicode置換文字U+FFFDのJSONエスケープシーケンスである \ufffd を出力バッファに書き込みます。
  3. i += size: isize (この場合は1) だけ進めます。これにより、不正なバイトがスキップされ、次の文字の処理に進みます。
  4. start = i: start を現在の i の位置に更新します。これは、次の有効な文字列部分の開始点となります。
  5. continue: ループの次のイテレーションに進み、文字列の残りの部分の処理を続行します。

この変更により、Marshal は無効なUTF-8バイトを検出しても処理を中断せず、代わりに \ufffd を挿入することで、常に有効なUTF-8を含むJSON出力を生成するようになりました。

関連リンク

参考にした情報源リンク

[インデックス 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)と標準ライブラリ内の一貫性の向上です。

  1. 実用性の問題: 以前の 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」という記述が、この問題意識を端的に表しています。

  2. 標準ライブラリ内の一貫性: Goの標準ライブラリの他の多くの部分、例えば io.Readerbufio.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を許容し、置換文字に変換する「より寛容な」挙動へと変更されました。

前提知識の解説

このコミットを理解するためには、以下の概念について理解しておく必要があります。

  1. UTF-8:

    • UTF-8は、Unicode文字をバイトシーケンスにエンコードするための可変長文字エンコーディングです。ASCII文字は1バイトで表現され、他の多くの文字は2バイトから4バイトで表現されます。
    • UTF-8は自己同期性(self-synchronizing)を持つように設計されており、バイトストリームの途中で読み取りを開始しても、次の有効な文字の開始位置を比較的容易に特定できます。
    • しかし、バイトシーケンスがUTF-8の仕様に準拠していない場合、それは「無効なUTF-8シーケンス」と見なされます。例えば、マルチバイト文字の途中でバイトが欠落したり、不正なバイト値が出現したりする場合です。
  2. Unicode置換文字 (U+FFFD):

    • U+FFFD () は、Unicodeの「Replacement Character」(置換文字)です。
    • これは、テキスト処理において、エンコーディングエラーや、文字セットで表現できない文字が検出された場合に、その不正な文字の代わりに挿入される特殊な文字です。
    • この文字が挿入されることで、不正なデータが含まれていても、処理を中断せずに続行できるようになります。
  3. JSON (JavaScript Object Notation):

    • JSONは、人間が読み書きしやすく、機械が解析しやすいデータ交換フォーマットです。
    • JSONの文字列値は、Unicode文字のシーケンスとして定義されており、通常はUTF-8でエンコードされます。JSON仕様では、文字列はUnicode文字のシーケンスであり、特定の制御文字(U+0000からU+001F)はエスケープする必要があります。
  4. Goの encoding/json パッケージ:

    • Go言語の標準ライブラリに含まれるパッケージで、Goのデータ構造とJSONデータの間で変換(マーシャリングとアンマーシャリング)を行う機能を提供します。
    • json.Marshal 関数は、Goの任意の値をJSON形式のバイトスライスに変換します。
    • json.Unmarshal 関数は、JSON形式のバイトスライスをGoのデータ構造に変換します。
  5. utf8.DecodeRuneInString 関数:

    • Goの unicode/utf8 パッケージに含まれる関数で、文字列の先頭からUTF-8エンコードされたルーン(Unicodeコードポイント)をデコードします。
    • この関数は、デコードされたルーンと、そのルーンを構成するバイト数(サイズ)を返します。
    • もし入力が有効なUTF-8シーケンスでなかった場合、utf8.RuneError (U+FFFD) と、その不正なシーケンスを構成するバイト数(通常は1)を返します。この utf8.RuneErrorsize == 1 の組み合わせは、単一の不正なバイトが検出されたことを示します。

技術的詳細

このコミットが行われる前、encoding/json.Marshal は、Goの文字列をJSON文字列としてエンコードする際に、その文字列が厳密に有効なUTF-8であるかを検査していました。もし文字列内に無効なUTF-8バイトシーケンス(例えば、\xff のような単一の不正なバイト)が検出された場合、Marshal*json.InvalidUTF8Error 型のエラーを即座に返し、JSONの生成を中断していました。これは、データソースが完全にUTF-8準拠でない場合に、アプリケーションがJSONを生成できないという問題を引き起こしていました。

このコミットによって、Marshal の挙動は以下のように変更されました。

  1. エラーの廃止: InvalidUTF8Error は、もはや Marshal によって生成されなくなりました。ただし、後方互換性のために型定義自体は残されています。これは、既存のコードがこのエラー型を参照している場合にコンパイルエラーにならないようにするためです。
  2. 置換文字への変換: Marshal は、文字列を走査する際に utf8.DecodeRuneInString を使用して各ルーンをデコードします。
    • もし utf8.DecodeRuneInStringutf8.RuneErrorsize == 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 を伴って中断されていました。

変更後: エラーを返す代わりに、以下の処理が行われます。

  1. if start < i { e.WriteString(s[start:i]) }: start は最後に有効な文字が書き込まれた位置、i は現在の不正なバイトの位置です。この条件が真の場合、start から i までの有効な文字列部分をまず出力バッファに書き込みます。これにより、不正なバイトの手前までの文字列が正しく処理されます。
  2. e.WriteString(\ufffd): ここが最も重要な変更点です。不正なUTF-8バイトシーケンスの代わりに、Unicode置換文字U+FFFDのJSONエスケープシーケンスである \ufffd を出力バッファに書き込みます。
  3. i += size: isize (この場合は1) だけ進めます。これにより、不正なバイトがスキップされ、次の文字の処理に進みます。
  4. start = i: start を現在の i の位置に更新します。これは、次の有効な文字列部分の開始点となります。
  5. continue: ループの次のイテレーションに進み、文字列の残りの部分の処理を続行します。

この変更により、Marshal は無効なUTF-8バイトを検出しても処理を中断せず、代わりに \ufffd を挿入することで、常に有効なUTF-8を含むJSON出力を生成するようになりました。

関連リンク

参考にした情報源リンク