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

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

このコミットは、Go言語のコンパイラ(cmd/gc)における文字列変換の挙動を修正し、特にサロゲートペアの扱いをUnicodeの仕様に準拠させるためのものです。不正なUnicodeコードポイントやサロゲートペアが文字列に変換される際に、Unicodeの置換文字(U+FFFD)を正しく生成するように変更されています。この修正は、lib9のrune(ルーン)処理コードに施されています。

コミット

commit 363ec80dec5908ed7feebba448dc8e5b2cf90740
Author: Rob Pike <r@golang.org>
Date:   Thu Aug 30 11:16:55 2012 -0700

    cmd/gc: string conversion for surrogates
    This is required by the spec to produce the replacement char.
    The fix lies in lib9's rune code.
    
    R=golang-dev, nigeltao, rsc
    CC=golang-dev
    https://golang.org/cl/6443109

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

https://github.com/golang/go/commit/363ec80dec5908ed7feebba448dc8e5b2cf90740

元コミット内容

cmd/gc: string conversion for surrogates This is required by the spec to produce the replacement char. The fix lies in lib9's rune code.

変更の背景

この変更の背景には、Go言語がUnicodeの文字列を扱う上での正確性と仕様遵守があります。Unicodeには、サロゲートペアと呼ばれる特殊なコードポイントの範囲(U+D800からU+DFFF)が存在します。これらは単独では有効な文字を表さず、UTF-16エンコーディングにおいて補助文字(サロゲートペアで表現されるU+10000以上の文字)を表現するために使用されます。しかし、UTF-8エンコーディングにおいては、サロゲートペアのコードポイントは不正なシーケンスとして扱われるべきであり、文字列変換時にこれらが現れた場合、Unicodeの仕様に従って「置換文字」(Replacement Character, U+FFFD)に置き換えられる必要があります。

以前のGoのlib9のrune処理コードでは、このサロゲートペアの扱いが不完全であった可能性があります。具体的には、不正なUTF-8シーケンスや、単独で現れるサロゲートコードポイントが、期待通りにU+FFFDに変換されず、予期せぬ結果を引き起こす可能性がありました。このコミットは、この問題を修正し、Go言語がUnicodeの文字列処理においてより堅牢で仕様に準拠した挙動を示すようにすることを目的としています。

また、string(rune)のようなGoの組み込み関数やコンパイラが、不正なrune値(例えば、Unicodeの範囲外の値やサロゲートペア)を受け取った際に、どのような文字列を生成すべきかという仕様上の要件を満たす必要がありました。この修正は、これらのケースで一貫してU+FFFDを生成するようにすることで、Goプログラムの予測可能性と信頼性を向上させます。

前提知識の解説

UnicodeとUTF-8

  • Unicode: 世界中の文字を統一的に扱うための文字コード標準です。各文字には一意の「コードポイント」(例: U+0041は'A')が割り当てられています。
  • UTF-8: Unicodeのコードポイントをバイト列にエンコードするための可変長エンコーディング方式です。ASCII文字は1バイト、それ以外の文字は2バイト以上で表現されます。UTF-8は自己同期性があり、部分的に破損しても残りの部分を読み続けやすいという特徴があります。

Rune (ルーン)

Go言語において、rune型はUnicodeのコードポイントを表すために使用される組み込み型です。これはint32のエイリアスであり、1つのruneが1つのUnicode文字に対応します。Goの文字列はUTF-8でエンコードされたバイト列ですが、for rangeループなどで文字列をイテレートすると、各要素はruneとして扱われます。

サロゲートペア (Surrogate Pairs)

Unicodeには、基本多言語面(BMP, Basic Multilingual Plane, U+0000からU+FFFF)と呼ばれる範囲があります。この範囲外の文字(補助文字、U+10000からU+10FFFF)をUTF-16でエンコードするために、「サロゲートペア」という仕組みが導入されました。

  • 上位サロゲート (High Surrogate): U+D800からU+DBFFの範囲のコードポイント。
  • 下位サロゲート (Low Surrogate): U+DC00からU+DFFFの範囲のコードポイント。 これら2つのコードポイントがペアになることで、1つの補助文字を表します。しかし、UTF-8エンコーディングにおいては、これらのサロゲートコードポイントは単独で現れることはなく、もし現れた場合は不正なシーケンスとして扱われます。

置換文字 (Replacement Character, U+FFFD)

Unicodeの置換文字(U+FFFD)は、不正な文字エンコーディングや、文字コード変換で対応する文字がない場合に、その文字の代わりに表示される特殊な文字です。UTF-8では、U+FFFDは\xef\xbf\xbdという3バイトでエンコードされます。Unicodeの仕様では、不正なUTF-8シーケンスや、単独で現れるサロゲートコードポイントは、この置換文字に置き換えられるべきだと定められています。

lib9

lib9は、Plan 9オペレーティングシステムに由来するライブラリ群で、Go言語のランタイムや標準ライブラリの一部で利用されています。特に、文字エンコーディングやファイルシステム関連の低レベルな処理にその影響が見られます。このコミットで修正されているsrc/lib9/utf/rune.cは、UTF-8とrune間の変換を行うC言語のコードです。

技術的詳細

このコミットの技術的詳細は、主にsrc/lib9/utf/rune.c内のUTF-8デコード/エンコードロジックの変更に集約されます。

  1. サロゲート範囲の定義: rune.cSurrogateMin (0xD800) と SurrogateMax (0xDFFF) という定数が追加されました。これにより、サロゲートペアのコードポイント範囲が明確に定義されます。

  2. charntorune および chartorune の修正: これらの関数は、バイト列からruneをデコードする役割を担っています。

    • サロゲートチェックの追加: デコードされたruneがSurrogateMinからSurrogateMaxの範囲内にある場合、そのruneは不正なものとして扱われ、goto bad;にジャンプします。badラベルでは、*rune = Bad;BadRuneerror、つまりU+FFFD)が設定され、不正なシーケンスとして処理されます。
    • Runemaxチェックの強化: 4バイトシーケンスのデコードにおいて、デコードされたruneがRunemax(Unicodeの最大コードポイントであるU+10FFFF)を超える場合も不正なものとして扱われるようになりました。これは、Unicodeの有効なコードポイント範囲外の値を正しく処理するためです。
  3. runetochar の修正: この関数は、runeをバイト列(UTF-8)にエンコードする役割を担っています。

    • サロゲート変換の追加: エンコードしようとしているruneがSurrogateMinからSurrogateMaxの範囲内にある場合、そのruneはエンコード前にRuneerror(U+FFFD)に変換されます。これにより、Goのstring(rune)変換において、サロゲートコードポイントが渡された場合に、正しくU+FFFDのUTF-8表現(\xef\xbf\xbd)が生成されるようになります。

これらの変更により、Goの文字列変換システムは、Unicodeの仕様に厳密に準拠し、不正なコードポイントやサロゲートペアを適切に置換文字に変換するようになりました。

テストファイルsrc/pkg/unicode/utf8/utf8_test.gotest/string_lit.goには、これらの新しい挙動を検証するためのテストケースが追加されています。特にtest/string_lit.goでは、string(0xD800)string(0xDFFF)といったサロゲートコードポイントを文字列に変換した際に、期待通り\xef\xbf\xbdが生成されることを確認するテストが追加されています。

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

src/lib9/utf/rune.c

--- a/src/lib9/utf/rune.c
+++ b/src/lib9/utf/rune.c
@@ -36,12 +36,14 @@ enum
  	Rune1	= (1<<(Bit1+0*Bitx))-1,		/* 0000 0000 0111 1111 */
  	Rune2	= (1<<(Bit2+1*Bitx))-1,		/* 0000 0111 1111 1111 */
  	Rune3	= (1<<(Bit3+2*Bitx))-1,		/* 1111 1111 1111 1111 */
-	Rune4	= (1<<(Bit4+3*Bitx))-1,
-                                        /* 0001 1111 1111 1111 1111 1111 */
+	Rune4	= (1<<(Bit4+3*Bitx))-1,		/* 0001 1111 1111 1111 1111 1111 */
 
  	Maskx	= (1<<Bitx)-1,			/* 0011 1111 */
  	Testx	= Maskx ^ 0xFF,			/* 1100 0000 */
 
+	SurrogateMin	= 0xD800,
+	SurrogateMax	= 0xDFFF,
+
  	Bad	= Runeerror,
  };
 
@@ -122,6 +124,8 @@ charntorune(Rune *rune, const char *str, int length)
  		tl = ((((c << Bitx) | c1) << Bitx) | c2) & Rune3;
  		if(l <= Rune2)
  			goto bad;
+		if (SurrogateMin <= l && l <= SurrogateMax)
+			goto bad;
  		*rune = l;
  		return 3;
  	}
@@ -138,7 +142,7 @@ charntorune(Rune *rune, const char *str, int length)
  		goto bad;
  	if (c < T5) {
  		l = ((((((c << Bitx) | c1) << Bitx) | c2) << Bitx) | c3) & Rune4;
-		if (l <= Rune3)
+		if (l <= Rune3 || l > Runemax)
  			goto bad;
  		*rune = l;
  		return 4;
@@ -208,6 +212,8 @@ chartorune(Rune *rune, const char *str)
  		tl = ((((c << Bitx) | c1) << Bitx) | c2) & Rune3;
  		if(l <= Rune2)
  			goto bad;
+		if (SurrogateMin <= l && l <= SurrogateMax)
+			goto bad;
  		*rune = l;
  		return 3;
  	}
@@ -221,7 +227,7 @@ chartorune(Rune *rune, const char *str)
  		goto bad;
  	if (c < T5) {
  		l = ((((((c << Bitx) | c1) << Bitx) | c2) << Bitx) | c3) & Rune4;
-		if (l <= Rune3)
+		if (l <= Rune3 || l > Runemax)
  			goto bad;
  		*rune = l;
  		return 4;
@@ -273,13 +279,15 @@ runetochar(char *str, const Rune *rune)
  	}
 
  	/*
-	 * If the Rune is out of range, convert it to the error rune.
+	 * If the Rune is out of range or a surrogate half, convert it to the error rune.
 	 * Do this test here because the error rune encodes to three bytes.
 	 * Doing it earlier would duplicate work, since an out of range
 	 * Rune wouldn\'t have fit in one or two bytes.
 	 */
  	if (c > Runemax)
  		c = Runeerror;
+	if (SurrogateMin <= c && c <= SurrogateMax)
+		c = Runeerror;
 
  	/*
  	 * three character sequence

src/pkg/unicode/utf8/utf8_test.go

--- a/src/pkg/unicode/utf8/utf8_test.go
+++ b/src/pkg/unicode/utf8/utf8_test.go
@@ -69,7 +69,7 @@ var utf8map = []Utf8Map{
 
 var surrogateMap = []Utf8Map{
 	{0xd800, "\\xed\\xa0\\x80"}, // surrogate min decodes to (RuneError, 1)
-	{0xdfff, "\\xed bf bf"},   // surrogate max decodes to (RuneError, 1)
+	{0xdfff, "\\xed\\xbf\\xbf"}, // surrogate max decodes to (RuneError, 1)
 }
 
 var testStrings = []string{
@@ -355,7 +355,9 @@ var validTests = []ValidTest{
 	{string([]byte{66, 250}), false},
 	{string([]byte{66, 250, 67}), false},
 	{"a\\uFFFDb", true},
-	{string("\\xF7\\xBF\\xBF\\xBF"), true},      // U+1FFFFF
+	{string("\\xF4\\x8F\\xBF\\xBF"), true},      // U+10FFFF
+	{string("\\xF4\\x90\\x80\\x80"), false},     // U+10FFFF+1; out of range
+	{string("\\xF7\\xBF\\xBF\\xBF"), false},     // 0x1FFFFF; out of range
 	{string("\\xFB\\xBF\\xBF\\xBF\\xBF"), false}, // 0x3FFFFFF; out of range
 	{string("\\xc0\\x80"), false},             // U+0000 encoded in two bytes: incorrect
 	{string("\\xed\\xa0\\x80"), false},         // U+D800 high surrogate (sic)

test/string_lit.go

--- a/test/string_lit.go
+++ b/test/string_lit.go
@@ -93,7 +93,7 @@ func main() {
  		"backslashes 2 (backquote)")
  	assert("\\x\\u\\U\\", `\x\u\U\`, "backslash 3 (backquote)")
 
-	// test large runes. perhaps not the most logical place for this test.
+	// test large and surrogate-half runes. perhaps not the most logical place for these tests.
  	var r int32
  	r = 0x10ffff // largest rune value
  	s = string(r)
@@ -101,6 +101,28 @@ func main() {
  	r = 0x10ffff + 1
  	s = string(r)
  	assert(s, "\\xef\\xbf\\xbd", "too-large rune")
+	r = 0xD800
+	s = string(r)
+	assert(s, "\\xef\\xbf\\xbd", "surrogate rune min")
+	r = 0xDFFF
+	s = string(r)
+	assert(s, "\\xef\\xbf\\xbd", "surrogate rune max")
+	r = -1
+	s = string(r)
+	assert(s, "\\xef\\xbf\\xbd", "negative rune")
+	
+	// the large rune tests again, this time using constants instead of a variable.
+	// these conversions will be done at compile time.
+	s = string(0x10ffff) // largest rune value
+	assert(s, "\\xf4\\x8f\\xbf\\xbf", "largest rune constant")
+	s = string(0x10ffff + 1)
+	assert(s, "\\xef\\xbf\\xbd", "too-large rune constant")
+	s = string(0xD800)
+	assert(s, "\\xef\\xbf\\xbd", "surrogate rune min constant")
+	s = string(0xDFFF)
+	assert(s, "\\xef\\xbf\\xbd", "surrogate rune max constant")
+	s = string(-1)
+	assert(s, "\\xef\\xbf\\xbd", "negative rune")
 
  	assert(string(gr1), gx1, "global ->[]rune")
  	assert(string(gr2), gx2fix, "global invalid ->[]rune")

コアとなるコードの解説

src/lib9/utf/rune.c

このファイルは、Go言語の低レベルなUTF-8とruneの変換ロジックをC言語で実装しています。

  • enum 定義の追加: SurrogateMin = 0xD800, SurrogateMax = 0xDFFF, これらの定数は、Unicodeのサロゲートペアの範囲を明確に定義します。これにより、コード内でマジックナンバーを使うことなく、サロゲート範囲のチェックが可能になります。

  • charntorune および chartorune 関数内の変更: これらの関数は、バイト列(UTF-8エンコードされた文字列)からUnicodeコードポイント(rune)を読み取る役割を担います。

    if (SurrogateMin <= l && l <= SurrogateMax)
        goto bad;
    

    この行が追加されたことで、デコードされたl(rune)がサロゲート範囲内にある場合、即座にbadラベルにジャンプします。badラベルでは、*rune = Bad;BadRuneerror、つまりU+FFFD)が設定され、不正なシーケンスとして処理されます。これは、UTF-8においてサロゲートコードポイントが単独で現れることは不正であるというUnicodeの仕様に準拠するためです。

    if (l <= Rune3 || l > Runemax)
    

    4バイトシーケンスをデコードする部分で、l > Runemaxという条件が追加されました。RunemaxはUnicodeの最大コードポイント(U+10FFFF)を表します。これにより、Unicodeの有効な範囲を超えるコードポイントがデコードされた場合も、不正なものとしてbadにジャンプし、U+FFFDに変換されるようになります。

  • runetochar 関数内の変更: この関数は、Unicodeコードポイント(rune)をバイト列(UTF-8エンコードされた文字列)に変換する役割を担います。

    if (SurrogateMin <= c && c <= SurrogateMax)
        c = Runeerror;
    

    この行が追加されたことで、エンコードしようとしているrune c がサロゲート範囲内にある場合、そのruneはRuneerror(U+FFFD)に置き換えられます。これは、Goのstring(rune)変換において、サロゲートコードポイントが渡された場合に、正しくU+FFFDのUTF-8表現(\xef\xbf\xbd)が生成されるようにするためです。

src/pkg/unicode/utf8/utf8_test.go

このファイルは、Goのunicode/utf8パッケージのテストケースを含んでいます。

  • surrogateMapのコメント修正: // surrogate max decodes to (RuneError, 1) のコメントが修正され、バックスラッシュがエスケープされるようになりました。
  • validTestsに新しいテストケースが追加されました。特に、U+10FFFF+1(Unicodeの最大値を超える)や0x1FFFFF(さらに大きな範囲外の値)がfalse(無効)として扱われることを検証しています。これは、rune.cl > Runemaxチェックと関連しています。

test/string_lit.go

このファイルは、Goの文字列リテラルやstring()変換の挙動をテストするためのものです。

  • コメントの更新: // test large and surrogate-half runes. となり、サロゲートハーフのruneもテスト対象であることが明示されました。
  • 新しいassertテストの追加:
    • r = 0xD800; s = string(r); assert(s, "\\xef\\xbf\\xbd", "surrogate rune min")
    • r = 0xDFFF; s = string(r); assert(s, "\\xef\\xbf\\xbd", "surrogate rune max")
    • r = -1; s = string(r); assert(s, "\\xef\\xbf\\xbd", "negative rune") これらのテストは、サロゲートコードポイントや負のrune値をstring()関数で変換した場合に、期待通りにU+FFFDのUTF-8表現(\xef\xbf\xbd)が生成されることを検証しています。これは、runetochar関数の修正によって実現される挙動です。
    • 定数を使った同様のテストも追加されており、コンパイル時にも同じ変換が行われることを確認しています。

これらの変更とテストの追加により、Go言語はUnicodeの文字列処理において、より正確で堅牢な挙動を提供するようになりました。

関連リンク

参考にした情報源リンク