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

[インデックス 15251] ファイルの概要

このコミットは、Go言語の encoding/json パッケージにおけるJSON文字列のデコード処理において、不正なUTF-8シーケンスや不正なUTF-16サロゲートペアがどのように扱われるかを明確にするためのドキュメントの追加と、その挙動を検証するテストケースの追加を目的としています。具体的には、これらの不正な文字がUnicodeの置換文字 (U+FFFD) に置き換えられることを明記し、その挙動をテストで確認しています。

コミット

commit 30359a55c264dba2076c86f40a3c7c915889b9df
Author: Russ Cox <rsc@golang.org>
Date:   Thu Feb 14 14:56:01 2013 -0500

    encoding/json: document and test use of unicode.ReplacementChar

    Fixes #4783.

    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/7314099

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/30359a55c264dba2076c86f40a3c7c915889b9df

元コミット内容

encoding/json: unicode.ReplacementChar の使用をドキュメント化し、テストを追加

Issue #4783 を修正。

変更の背景

この変更の背景には、Go言語の encoding/json パッケージがJSON文字列をデコードする際に、不正なUTF-8バイトシーケンスや不正なUTF-16サロゲートペアに遭遇した場合の挙動が不明確であったという問題があります。JSON仕様(RFC 7159など)では、文字列はUnicode文字のシーケンスとして定義されており、UTF-8でエンコードされることが一般的です。しかし、実際のデータには、エンコーディングの誤りや破損によって不正なバイトシーケンスが含まれることがあります。

このような不正なデータに遭遇した際に、encoding/json パッケージがエラーを返すのか、それとも何らかの形で回復を試みるのかが明確ではありませんでした。特に、Goの文字列は常に有効なUTF-8であるという前提があるため、不正なバイトシーケンスをどのように処理するかが重要になります。

このコミットは、この不明確さを解消し、不正な文字シーケンスがUnicodeの置換文字 (U+FFFD) に置き換えられるという既存の挙動を明示的にドキュメント化し、その挙動を保証するためのテストを追加することで、パッケージの堅牢性と予測可能性を高めることを目的としています。これにより、開発者は encoding/json パッケージが不正な入力に対してどのように振る舞うかを正確に理解し、それに応じた処理を実装できるようになります。

前提知識の解説

1. UTF-8とUnicode

  • Unicode: 世界中のあらゆる文字を統一的に扱うための文字コードの国際標準です。各文字には一意の「コードポイント」(例: U+0041は'A')が割り当てられています。
  • UTF-8: Unicodeの文字をバイト列にエンコードするための可変長エンコーディング方式の一つです。ASCII文字は1バイトで表現され、他の文字は2バイトから4バイトで表現されます。UTF-8の大きな特徴は、自己同期性があり、不正なバイトシーケンスを検出・スキップしやすい点です。

2. 不正なUTF-8シーケンス

UTF-8には厳密なエンコーディングルールがあります。例えば、マルチバイト文字の開始バイトと後続バイトには特定のビットパターンが必要です。これらのルールに従わないバイトシーケンスは「不正なUTF-8シーケンス」と見なされます。

例:

  • \xff: 単独の不正なバイト。UTF-8では1バイト文字は0x00-0x7Fの範囲に限定されます。
  • \xc2\xc2: 2バイト文字の開始バイト (\xc2) の後に、不正な後続バイト (\xc2) が続く例。後続バイトは0x80-0xBFの範囲である必要があります。
  • \xc2\xff: 同様に、不正な後続バイトの例。

3. UTF-16サロゲートペア

  • UTF-16: Unicodeの文字を16ビット単位でエンコードする方式です。基本多言語面 (BMP) の文字は16ビットで表現されますが、それ以外の文字(サロゲートペア)は2つの16ビット値(上位サロゲートと下位サロゲート)のペアで表現されます。
  • 不正なUTF-16サロゲートペア: サロゲートペアは常に上位サロゲートと下位サロゲートがセットで出現する必要があります。どちらか一方だけが存在する場合や、不正な組み合わせの場合、「不正なUTF-16サロゲートペア」と見なされます。JSON文字列では、\uXXXX の形式でUnicodeエスケープシーケンスが表現されますが、これが不正なサロゲートペアを形成することがあります。

例:

  • \ud800: 上位サロゲート単独。下位サロゲートが欠けているため不正です。

4. Unicode置換文字 (U+FFFD)

  • U+FFFD (REPLACEMENT CHARACTER): Unicodeの特殊な文字で、エンコーディング変換中に文字が認識できない、または表現できない場合に、その文字の代わりに挿入される記号です。通常、菱形の中に疑問符が書かれた記号として表示されます。これは、データが破損していることを示しつつも、処理を続行するためのメカニズムとして機能します。

5. Go言語の文字列とUTF-8

Go言語の文字列は、内部的にはバイトの読み取り専用スライスであり、慣例的にUTF-8でエンコードされたテキストを保持するとされています。Goの標準ライブラリは、文字列操作においてUTF-8の妥当性を重視しており、不正なUTF-8シーケンスを検出した場合、多くの場合U+FFFDに置き換えることで、常に有効なUTF-8文字列を維持しようとします。

技術的詳細

このコミットは、Goの encoding/json パッケージがJSON文字列をデコードする際の、不正な文字エンコーディングに対する回復戦略を明確にしています。JSON仕様では文字列はUnicode文字のシーケンスであり、通常はUTF-8でエンコードされます。しかし、現実のデータには、様々な理由で不正なバイトシーケンスが含まれることがあります。

encoding/json パッケージは、このような不正なUTF-8バイトシーケンスや、JSONの \uXXXX エスケープシーケンスで表現される不正なUTF-16サロゲートペアに遭遇した場合、エラーを返してデコード処理を中断するのではなく、Goの unicode.ReplacementChar (U+FFFD) に置き換えることで処理を続行します。この挙動は、Goの標準ライブラリにおけるUTF-8処理の一般的なアプローチと一致しています。Goの文字列は常に有効なUTF-8であるべきという原則に基づき、不正なバイトシーケンスはU+FFFDに「矯正」されます。

このアプローチの利点は以下の通りです。

  1. 堅牢性: 不正な入力データに対してもパニックを起こさず、可能な限りデコード処理を完了させることができます。これにより、部分的に破損したJSONデータでも有用な情報を抽出できる可能性があります。
  2. 予測可能性: 不正な文字がどのように扱われるかが明確になるため、開発者はその挙動を前提としたコードを記述できます。
  3. 一貫性: Go言語全体でのUTF-8処理の一貫性を保ちます。

一方で、このアプローチには以下の考慮事項があります。

  • データの損失: 不正な文字はU+FFFDに置き換えられるため、元の不正なバイトシーケンスに関する情報は失われます。これは、厳密なデータ検証が必要な場合には問題となる可能性があります。
  • サイレントな修正: エラーが返されないため、不正なデータが入力されたことに気づかない可能性があります。アプリケーションによっては、U+FFFDが挿入されたことを検出して、適切なエラー処理やログ記録を行う必要があるかもしれません。

このコミットでは、decode.go にコメントを追加することでこの挙動を明示的にドキュメント化し、decode_test.go に様々な不正なUTF-8シーケンスやUTF-16サロゲートペアを含むテストケースを追加することで、この挙動が意図通りに機能することを保証しています。これにより、encoding/json パッケージの信頼性と使いやすさが向上しています。

コアとなるコードの変更箇所

このコミットによるコードの変更は主に2つのファイルにわたります。

  1. src/pkg/encoding/json/decode.go: Unmarshal 関数のドキュメントコメントに、不正なUTF-8やUTF-16サロゲートペアの扱いに関する記述が追加されました。

    --- a/src/pkg/encoding/json/decode.go
    +++ b/src/pkg/encoding/json/decode.go
    @@ -55,6 +55,11 @@ import (
     // If no more serious errors are encountered, Unmarshal returns
     // an UnmarshalTypeError describing the earliest such error.
     //
    +// When unmarshaling quoted strings, invalid UTF-8 or
    +// invalid UTF-16 surrogate pairs are not treated as an error.
    +// Instead, they are replaced by the Unicode replacement
    +// character U+FFFD.
    +//
      func Unmarshal(data []byte, v interface{}) error {
      	// Check for well-formedness.
      	// Avoids filling out half a data structure
    
  2. src/pkg/encoding/json/decode_test.go: unmarshalTests 変数に、不正なUTF-8シーケンスや不正なUTF-16サロゲートペアを含むJSON文字列をデコードする際の挙動を検証するための新しいテストケースが多数追加されました。

    --- a/src/pkg/encoding/json/decode_test.go
    +++ b/src/pkg/encoding/json/decode_test.go
    @@ -330,6 +330,43 @@ var unmarshalTests = []unmarshalTest{\n
     		ptr: new(S10),\n
     		out: S10{S13: S13{S8: S8{S9: S9{Y: 2}}}},\n
     	},\n
    +\n+\t// invalid UTF-8 is coerced to valid UTF-8.\n
    +\t{\n
    +\t\tin:  "\"hello\\xffworld\"",\n
    +\t\tptr: new(string),\n
    +\t\tout: "hello\\ufffdworld",\n
    +\t},\n
    +\t{\n
    +\t\tin:  "\"hello\\xc2\\xc2world\"",\n
    +\t\tptr: new(string),\n
    +\t\tout: "hello\\ufffd\\ufffdworld",\n
    +\t},\n
    +\t{\n
    +\t\tin:  "\"hello\\xc2\\xffworld\"",\n
    +\t\tptr: new(string),\n
    +\t\tout: "hello\\ufffd\\ufffdworld",\n
    +\t},\n
    +\t{\n
    +\t\tin:  "\"hello\\\\ud800world\"",\n
    +\t\tptr: new(string),\n
    +\t\tout: "hello\\ufffdworld",\n
    +\t},\n
    +\t{\n
    +\t\tin:  "\"hello\\\\ud800\\\\ud800world\"",\n
    +\t\tptr: new(string),\n
    +\t\tout: "hello\\ufffd\\ufffdworld",\n
    +\t},\n
    +\t{\n
    +\t\tin:  "\"hello\\\\ud800\\\\ud800world\"",\n
    +\t\tptr: new(string),\n
    +\t\tout: "hello\\ufffd\\ufffdworld",\n
    +\t},\n
    +\t{\n
    +\t\tin:  "\"hello\\xed\\xa0\\x80\\xed\\xb0\\x80world\"",\n
    +\t\tptr: new(string),\n
    +\t\tout: "hello\\ufffd\\ufffd\\ufffd\\ufffd\\ufffd\\ufffdworld",\n
    +\t},\n
     }\n
     func TestMarshal(t *testing.T) {\n
    

コアとなるコードの解説

src/pkg/encoding/json/decode.go の変更

Unmarshal 関数のドキュメントコメントに追加された以下の5行は、encoding/json パッケージがJSON文字列をGoの文字列にデコードする際の重要な挙動を明文化しています。

// When unmarshaling quoted strings, invalid UTF-8 or
// invalid UTF-16 surrogate pairs are not treated as an error.
// Instead, they are replaced by the Unicode replacement
// character U+FFFD.

このコメントは、以下の点を明確にしています。

  • 対象: JSONの引用符で囲まれた文字列(JSON文字列リテラル)をGoの文字列型にアンマーシャルする際。
  • 挙動: 不正なUTF-8バイトシーケンスや、不正なUTF-16サロゲートペア(例: \uD800 のような単独のサロゲート)は、エラーとして扱われない。
  • 結果: これらの不正な文字は、Goの unicode.ReplacementChar (U+FFFD) に置き換えられる。

これは、Unmarshal 関数が不正な入力に対して堅牢であり、エラーを返さずに可能な限りデコードを続行するという設計思想を反映しています。開発者はこの挙動を理解し、必要に応じてデコード後の文字列にU+FFFDが含まれていないかを確認するなどの追加処理を検討できます。

src/pkg/encoding/json/decode_test.go の変更

unmarshalTests に追加された複数のテストケースは、上記のドキュメント化された挙動を具体的に検証するためのものです。これらのテストケースは、様々な種類の不正なエンコーディングを含むJSON文字列を入力として与え、期待される出力がU+FFFDに置き換えられた正しい文字列であることを確認しています。

各テストケースの構造は以下の通りです。

{
    in:  "\"hello\\xffworld\"", // 入力JSON文字列
    ptr: new(string),          // デコード先の型(ここではstringへのポインタ)
    out: "hello\\ufffdworld",  // 期待されるデコード結果の文字列
},

具体的なテストケースとその意味は以下の通りです。

  • "\"hello\\xffworld\"": 単独の不正なバイト \xff がU+FFFDに置き換えられることをテスト。
  • "\"hello\\xc2\\xc2world\"": 不正なUTF-8シーケンス(2バイト文字の開始バイトの後に不正な後続バイト)がU+FFFDに置き換えられることをテスト。
  • "\"hello\\xc2\\xffworld\"": 同様に、別の不正なUTF-8シーケンスがU+FFFDに置き換えられることをテスト。
  • "\"hello\\\\ud800world\"": 不正なUTF-16サロゲートペア(単独の上位サロゲート \ud800)がU+FFFDに置き換えられることをテスト。
  • "\"hello\\\\ud800\\\\ud800world\"": 複数の単独の上位サロゲートがそれぞれU+FFFDに置き換えられることをテスト。
  • "\"hello\\xed\\xa0\\x80\\xed\\xb0\\x80world\"": これは \ud800\udc00 に対応するUTF-8バイトシーケンスですが、JSON文字列リテラル内で直接不正なUTF-8バイトとして表現された場合(つまり、\uXXXX エスケープではなく、生のバイトとして扱われる場合)に、それぞれがU+FFFDに置き換えられることをテストしています。\xed\xa0\x80\ud800 のUTF-8表現、\xed\xb0\x80\udc00 のUTF-8表現ですが、これらが単独で現れると不正なUTF-8シーケンスとなります。

これらのテストケースは、encoding/json パッケージが様々な種類の不正な文字エンコーディングに対して、一貫してU+FFFDに置き換えるという挙動を保証するための重要な役割を果たしています。

関連リンク

  • Go Issue #4783: https://github.com/golang/go/issues/4783 このコミットが修正したGoのIssueトラッカーのエントリです。通常、問題の詳細な議論や背景情報が含まれています。
  • Gerrit Change 7314099: https://golang.org/cl/7314099 GoプロジェクトのコードレビューシステムであるGerritにおけるこの変更のリンクです。レビューコメントや変更の経緯が確認できます。

参考にした情報源リンク

  • Unicode Consortium: https://www.unicode.org/ Unicodeの公式ウェブサイト。Unicode標準、UTF-8、U+FFFDなどに関する詳細な情報源です。
  • RFC 7159 - The JavaScript Object Notation (JSON) Data Interchange Format: https://tools.ietf.org/html/rfc7159 JSONの公式仕様。文字列のエンコーディングに関する規定が含まれています。
  • Go言語のドキュメント (encoding/jsonパッケージ): https://pkg.go.dev/encoding/json Goの encoding/json パッケージの公式ドキュメント。このコミットで追加されたコメントもここに反映されています。
  • Go言語の文字列とUTF-8に関するブログ記事やチュートリアル:
    • A. A. K. (2012). Strings, bytes, runes and characters in Go. The Go Blog. https://go.dev/blog/strings
    • Go言語における文字列と文字コードの扱いについて解説している記事。Goの文字列がUTF-8を前提としていることや、不正なUTF-8の扱いについて理解を深めるのに役立ちます。
  • Wikipedia - Unicode置換文字: https://ja.wikipedia.org/wiki/Unicode%E7%BD%AE%E6%8F%9B%E6%96%87%E5%AD%97 U+FFFDに関する一般的な情報。
  • Wikipedia - UTF-8: https://ja.wikipedia.org/wiki/UTF-8 UTF-8エンコーディングに関する詳細な情報。
  • Wikipedia - UTF-16: https://ja.wikipedia.org/wiki/UTF-16 UTF-16エンコーディングとサロゲートペアに関する詳細な情報。