[インデックス 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デコード/エンコードロジックの変更に集約されます。
-
サロゲート範囲の定義:
rune.c
にSurrogateMin
(0xD800) とSurrogateMax
(0xDFFF) という定数が追加されました。これにより、サロゲートペアのコードポイント範囲が明確に定義されます。 -
charntorune
およびchartorune
の修正: これらの関数は、バイト列からruneをデコードする役割を担っています。- サロゲートチェックの追加: デコードされたruneが
SurrogateMin
からSurrogateMax
の範囲内にある場合、そのruneは不正なものとして扱われ、goto bad;
にジャンプします。bad
ラベルでは、*rune = Bad;
(Bad
はRuneerror
、つまりU+FFFD)が設定され、不正なシーケンスとして処理されます。 Runemax
チェックの強化: 4バイトシーケンスのデコードにおいて、デコードされたruneがRunemax
(Unicodeの最大コードポイントであるU+10FFFF)を超える場合も不正なものとして扱われるようになりました。これは、Unicodeの有効なコードポイント範囲外の値を正しく処理するためです。
- サロゲートチェックの追加: デコードされたruneが
-
runetochar
の修正: この関数は、runeをバイト列(UTF-8)にエンコードする役割を担っています。- サロゲート変換の追加: エンコードしようとしているruneが
SurrogateMin
からSurrogateMax
の範囲内にある場合、そのruneはエンコード前にRuneerror
(U+FFFD)に変換されます。これにより、Goのstring(rune)
変換において、サロゲートコードポイントが渡された場合に、正しくU+FFFDのUTF-8表現(\xef\xbf\xbd
)が生成されるようになります。
- サロゲート変換の追加: エンコードしようとしているruneが
これらの変更により、Goの文字列変換システムは、Unicodeの仕様に厳密に準拠し、不正なコードポイントやサロゲートペアを適切に置換文字に変換するようになりました。
テストファイルsrc/pkg/unicode/utf8/utf8_test.go
とtest/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;
(Bad
はRuneerror
、つまり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.c
のl > 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の文字列処理において、より正確で堅牢な挙動を提供するようになりました。
関連リンク
- Go言語のUnicodeパッケージ: https://pkg.go.dev/unicode
- Go言語のUTF-8パッケージ: https://pkg.go.dev/unicode/utf8
- Unicode Consortium: https://home.unicode.org/
- Unicode Replacement Character (U+FFFD): https://www.unicode.org/L2/L2004/04279-replacement-char.pdf
参考にした情報源リンク
- Go CL 6443109: https://golang.org/cl/6443109
- UTF-8 - Wikipedia: https://ja.wikipedia.org/wiki/UTF-8
- Unicode サロゲートペア - Wikipedia: https://ja.wikipedia.org/wiki/%E3%82%B5%E3%83%AD%E3%82%B2%E3%83%BC%E3%83%88%E3%83%9A%E3%82%A2
- Go言語のrune型について: https://go.dev/blog/strings (Go公式ブログのStrings, bytes, runes, and characters in Go)
- The Go Programming Language Specification - String types: https://go.dev/ref/spec#String_types
- The Go Programming Language Specification - Conversions: https://go.dev/ref/spec#Conversions