[インデックス 13604] ファイルの概要
このコミットは、Go言語のランタイムおよびunicode/utf8
パッケージにおいて、Unicodeのサロゲートペア(Surrogate Pair)の片割れ(Surrogate Half)がUTF-8エンコーディングで不正な値として扱われるように変更を加えるものです。これにより、UTF-16の概念であるサロゲートペアがUTF-8の文脈で誤って解釈されることを防ぎ、より堅牢なUnicode処理を実現します。
コミット
commit c48b77b1b5e5ac38351487583a1082b7b73d0ffe
Author: Rob Pike <r@golang.org>
Date: Wed Aug 8 14:01:23 2012 -0700
all: make Unicode surrogate halves illegal as UTF-8
Surrogate halves are part of UTF-16 and should never appear in UTF-8.
(The rune that two combined halves represent in UTF-16 should
be encoded directly.)
Encoding: encode as RuneError.
Decoding: convert to RuneError, consume one byte.
This requires changing:
package unicode/utf8
runtime for range over string
Also added utf8.ValidRune and fixed bug in utf.RuneLen.
Fixes #3927.
R=golang-dev, rsc, bsiegert
CC=golang-dev
https://golang.org/cl/6458099
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c48b77b1b5e5ac38351487583a1082b7b73d0ffe
元コミット内容
このコミットの目的は、Unicodeのサロゲートペアの片割れ(サロゲートハーフ)がUTF-8エンコーディングにおいて不正な値として扱われるようにすることです。
- サロゲートハーフはUTF-16の一部であり、UTF-8には決して現れるべきではありません。
- (UTF-16で2つのサロゲートハーフが結合して表現するルーンは、UTF-8では直接エンコードされるべきです。)
- エンコーディング時:
RuneError
としてエンコードされます。 - デコーディング時:
RuneError
に変換され、1バイト消費されます。 - この変更は、
unicode/utf8
パッケージと、文字列に対するrange
ループのランタイムの挙動に影響します。 - また、
utf8.ValidRune
関数が追加され、utf.RuneLen
のバグが修正されました。 - この変更は、Issue #3927を修正します。
変更の背景
Unicodeは、世界中の文字を統一的に扱うための文字コード標準です。そのエンコーディング形式にはUTF-8、UTF-16、UTF-32などがあります。Go言語は内部的にUTF-8を強く推奨し、文字列はUTF-8バイト列として扱われます。
UTF-16では、基本多言語面(BMP: Basic Multilingual Plane, U+0000からU+FFFF)の文字は16ビットで表現されますが、それ以外の補助多言語面(SMP: Supplementary Multilingual Plane, U+10000以降)の文字は、2つの16ビット値のペア、すなわち「サロゲートペア」として表現されます。このサロゲートペアを構成する個々の16ビット値を「サロゲートハーフ」と呼びます。サロゲートハーフは、U+D800からU+DFFFの範囲に割り当てられています。
UTF-8の設計では、サロゲートハーフの範囲のコードポイントは有効なUnicode文字としては扱われません。UTF-8は可変長エンコーディングであり、U+10000以上の文字も直接エンコードできます。したがって、UTF-16のサロゲートペアを構成する個々のサロゲートハーフがUTF-8バイト列として現れることは、不正な状態を意味します。
このコミット以前は、Go言語のUTF-8処理において、サロゲートハーフが不正なバイト列として適切に扱われない可能性がありました。これにより、UTF-8の仕様に厳密に準拠しないデータが処理されたり、予期せぬ挙動を引き起こしたりするリスクがありました。この変更は、このような不正なサロゲートハーフを明確にエラーとして扱うことで、Go言語のUnicode処理の堅牢性と正確性を向上させることを目的としています。特に、range
ループで文字列をイテレートする際に、不正なサロゲートハーフが誤って有効なルーンとして解釈されることを防ぎます。
前提知識の解説
Unicodeと文字エンコーディング
- Unicode: 世界中の文字を統一的に扱うための文字コード標準です。各文字には一意の「コードポイント」(例: U+0041は'A')が割り当てられています。
- UTF-8: Unicodeの文字エンコーディング形式の一つで、可変長エンコーディングです。ASCII文字は1バイトで表現され、それ以外の文字は2〜4バイトで表現されます。Webや多くのシステムで広く利用されており、Go言語の文字列のデフォルトエンコーディングでもあります。
- UTF-16: Unicodeの文字エンコーディング形式の一つで、主にWindowsシステムやJavaなどで利用されます。基本多言語面(BMP)の文字は2バイトで表現されますが、それ以外の文字(補助多言語面)は「サロゲートペア」と呼ばれる4バイト(2つの2バイト値)で表現されます。
- UTF-32: Unicodeの文字エンコーディング形式の一つで、すべての文字を4バイトで表現します。固定長であるため処理は単純ですが、ファイルサイズが大きくなる傾向があります。
サロゲートペアとサロゲートハーフ
- サロゲートペア (Surrogate Pair): UTF-16において、U+10000以上のコードポイント(補助多言語面の文字)を表現するために使用される2つの16ビット値の組み合わせです。
- 上位サロゲート (High Surrogate): U+D800からU+DBFFの範囲の16ビット値。
- 下位サロゲート (Low Surrogate): U+DC00からU+DFFFの範囲の16ビット値。
- これら2つがペアになることで、1つの補助多言語面の文字を表現します。例えば、絵文字などがこれに該当します。
- サロゲートハーフ (Surrogate Half): サロゲートペアを構成する個々の上位サロゲートまたは下位サロゲートのことです。これらの値は、単独で有効なUnicode文字を表すことはありません。
UTF-8におけるサロゲートハーフの扱い
UTF-8の仕様では、U+D800からU+DFFFの範囲のコードポイントは「不正なコードポイント」とされています。これは、UTF-8がサロゲートペアの概念を持たず、U+10000以上の文字も直接エンコードできるためです。したがって、UTF-8バイト列の中にサロゲートハーフに相当するバイトシーケンスが現れることは、不正なUTF-8データであることを意味します。
Go言語におけるrune
とRuneError
rune
型: Go言語におけるrune
型は、Unicodeのコードポイントを表すために使用される組み込み型で、int32
のエイリアスです。1つのrune
は1つのUnicodeコードポイントに対応します。RuneError
:unicode/utf8
パッケージで定義されている定数で、0xFFFD
(U+FFFD)に相当します。これは「Replacement Character」(代替文字)と呼ばれる特殊なUnicode文字で、不正な文字やエンコーディングエラーが発生した場合に、その文字の代わりに表示されます。Go言語のUTF-8処理において、不正なバイトシーケンスが検出された場合、その部分がRuneError
に置き換えられます。
range
ループと文字列
Go言語のfor...range
ループを文字列に適用すると、文字列をUTF-8バイト列として解釈し、各有効なUnicodeコードポイント(rune
)とそれに対応するバイトオフセットを返します。この挙動は、文字列が常に有効なUTF-8であるという前提に基づいています。不正なバイトシーケンスが検出された場合、RuneError
が返されます。
技術的詳細
このコミットは、Go言語のランタイム(C言語で実装された部分)とunicode/utf8
パッケージ(Go言語で実装された部分)の両方に変更を加えています。
src/pkg/runtime/rune.c
の変更
このファイルは、Goランタイムにおけるルーン(Unicodeコードポイント)のエンコード・デコード処理を担当しています。
SurrogateMin
とSurrogateMax
の定義:
サロゲートハーフの範囲を定義する定数が追加されました。+ SurrogateMin = 0xD800, + SurrogateMax = 0xDFFF,
runtime·charntorune
関数の変更: この関数は、UTF-8バイト列からルーンをデコードする際に使用されます。3バイトのUTF-8シーケンスをデコードする部分に、デコードされたルーンがサロゲートハーフの範囲内にあるかどうかをチェックするロジックが追加されました。
もしデコードされたルーンがサロゲートハーフの範囲内であれば、+ if (SurrogateMin <= l && l <= SurrogateMax) + goto bad;
bad
ラベルにジャンプし、Runeerror
として処理されるようになります。これにより、ランタイムレベルで不正なサロゲートハーフが検出され、Runeerror
に変換されるようになります。runtime·runetochar
関数の変更: この関数は、ルーンをUTF-8バイト列にエンコードする際に使用されます。エンコードしようとしているルーンがRunemax
(最大ルーン値)を超えるか、またはサロゲートハーフの範囲内にある場合に、そのルーンをRuneerror
に変換するロジックが追加されました。
これにより、サロゲートハーフをエンコードしようとすると、自動的に+ if (SurrogateMin <= c && c <= SurrogateMax) + c = Runeerror;
Runeerror
に置き換えられます。
src/pkg/unicode/utf8/utf8.go
の変更
このファイルは、Go言語のunicode/utf8
パッケージの実装です。
surrogateMin
とsurrogateMax
の定義:
Goコード内でもサロゲートハーフの範囲を定義する定数が追加されました。+const ( + surrogateMin = 0xD800 + surrogateMax = 0xDFFF +)
decodeRuneInternal
およびdecodeRuneInStringInternal
関数の変更: これらの関数は、バイトスライスや文字列からルーンをデコードする内部関数です。3バイトのUTF-8シーケンスをデコードする部分に、デコードされたルーンがサロゲートハーフの範囲内にあるかどうかをチェックするロジックが追加されました。
もしデコードされたルーンがサロゲートハーフの範囲内であれば、+ if surrogateMin <= r && r <= surrogateMax { + return RuneError, 1, false + }
RuneError
を返し、サイズを1バイトとして処理します。これは、不正なバイトシーケンスとして1バイト消費し、RuneError
を返すという挙動です。RuneLen
関数の変更: この関数は、ルーンをUTF-8エンコードするのに必要なバイト数を返します。不正なルーン(負の値、サロゲートハーフの範囲、MaxRune
を超える値)に対して-1
を返すように修正されました。
特に、サロゲートハーフの範囲のルーンに対して+ case r < 0: + return -1 ... + case surrogateMin <= r && r <= surrogateMax: + return -1 ... + case r <= MaxRune: // 以前は rune4Max だった + return 4
-1
を返すことで、これらのルーンが有効なUTF-8エンコーディングを持たないことを明示します。また、rune4Max
がMaxRune
に変更され、より正確な最大ルーン値が使用されるようになりました。EncodeRune
関数の変更: この関数は、ルーンをUTF-8バイト列にエンコードします。エンコードしようとしているルーンがサロゲートハーフの範囲内にある場合に、そのルーンをRuneError
に変換するロジックが追加されました。
これにより、サロゲートハーフをエンコードしようとすると、自動的に+ if surrogateMin <= r && r <= surrogateMax { + r = RuneError + }
RuneError
に置き換えられます。ValidRune
関数の追加: この新しい関数は、与えられたルーンがUTF-8として合法的にエンコード可能かどうかを報告します。範囲外のコードポイントやサロゲートハーフは不正と判断されます。
これは、ルーンがUTF-8として有効かどうかを事前にチェックするための便利なユーティリティ関数です。+func ValidRune(r rune) bool { + switch { + case r < 0: + return false + case surrogateMin <= r && r <= surrogateMax: + return false + case r > MaxRune: + return false + } + return true +}
src/pkg/unicode/utf8/utf8_test.go
の変更
テストファイルには、サロゲートハーフの処理に関する新しいテストケースが追加されました。
utf8map
にサロゲートハーフの境界値(0xd7ff
,0xe000
)が追加され、その前後の値が正しくエンコード・デコードされることを確認しています。surrogateMap
という新しいテストデータが追加され、サロゲートハーフがRuneError
としてデコードされ、1バイト消費されることを明示的にテストしています。TestRuneLen
のテストケースが追加され、RuneLen
がサロゲートハーフに対して-1
を返すことを確認しています。TestValid
のテストケースが更新され、サロゲートハーフを含む文字列がValid
およびValidString
で不正と判断されることを確認しています。ValidRuneTest
とTestValidRune
が追加され、ValidRune
関数がサロゲートハーフに対してfalse
を返すことを確認しています。
test/stringrange.go
の変更
このテストファイルは、Go言語の文字列のrange
ループの挙動をテストするものです。
- サロゲートハーフを含む不正なUTF-8バイト列(例:
"a\xed\xa0\x80a"
)をrange
ループで処理した際に、不正な部分がutf8.RuneError
として正しくデコードされることを確認するテストが追加されました。
これにより、ランタイムの+ for _, c := range "a\xed\xa0\x80a" { + if c != 'a' && c != utf8.RuneError { + fmt.Printf("surrogate UTF-8 does not error: %U\n", c) + ok = false + } + }
range
ループがサロゲートハーフを正しくエラーとして扱うことが保証されます。
コアとなるコードの変更箇所
このコミットのコアとなる変更は、以下のファイルと関数に集中しています。
src/pkg/runtime/rune.c
:runtime·charntorune
: UTF-8デコード時にサロゲートハーフをRuneerror
として扱うロジックの追加。runtime·runetochar
: ルーンエンコード時にサロゲートハーフをRuneerror
に変換するロジックの追加。
src/pkg/unicode/utf8/utf8.go
:decodeRuneInternal
,decodeRuneInStringInternal
: UTF-8デコード時にサロゲートハーフをRuneError
として扱い、1バイト消費するロジックの追加。RuneLen
: サロゲートハーフに対して-1
を返すように修正。EncodeRune
: サロゲートハーフをRuneError
に変換するロジックの追加。ValidRune
: サロゲートハーフを不正なルーンとして判定する新しい関数の追加。
- テストファイル:
src/pkg/unicode/utf8/utf8_test.go
: サロゲートハーフのエンコード/デコード、RuneLen
、ValidRune
の挙動を検証する多数の新しいテストケース。test/stringrange.go
: 文字列のrange
ループがサロゲートハーフを正しくRuneError
として処理することを確認するテスト。
コアとなるコードの解説
このコミットの核心は、Go言語のUnicode処理において、UTF-16の概念であるサロゲートハーフがUTF-8の文脈で「不正な値」として厳密に扱われるようにすることです。
具体的には、以下の2つの主要な側面で変更が加えられました。
-
デコード時の挙動の統一:
runtime
層(rune.c
)とunicode/utf8
パッケージ層(utf8.go
)の両方で、UTF-8バイト列からルーンをデコードする際に、サロゲートハーフの範囲(U+D800からU+DFFF)に該当するコードポイントが検出された場合、それをRuneError
(U+FFFD)として処理するようにしました。- これにより、不正なサロゲートハーフを含むUTF-8バイト列が文字列として扱われた場合、
for...range
ループなどでイテレートすると、その不正な部分がRuneError
として返されるようになります。また、不正なバイトシーケンスとして1バイトのみを消費し、次の文字のデコードに進むことで、残りの有効なUTF-8バイト列の処理を妨げないようにしています。
-
エンコード時の挙動の統一と
ValidRune
の導入:runtime
層とunicode/utf8
パッケージ層の両方で、rune
をUTF-8バイト列にエンコードしようとする際に、そのrune
がサロゲートハーフの範囲内にある場合、自動的にRuneError
に変換してからエンコードするようにしました。これにより、意図せずサロゲートハーフがUTF-8バイト列として出力されることを防ぎます。RuneLen
関数も、サロゲートハーフに対しては有効なバイト長を返さず、-1
を返すように変更されました。これは、サロゲートハーフがUTF-8として有効なエンコーディングを持たないことを明確に示します。- 新しく導入された
utf8.ValidRune
関数は、特定のrune
がUTF-8として合法的にエンコード可能かどうかを判定します。これにより、開発者はエンコード前にルーンの有効性を簡単にチェックできるようになり、より堅牢なコードを書くことが可能になります。サロゲートハーフは、この関数によってfalse
と判定されます。
これらの変更により、Go言語はUTF-8の仕様にさらに厳密に準拠するようになり、異なるエンコーディング間でのデータのやり取りや、外部からの不正なUTF-8データの処理において、より予測可能で安全な挙動を提供するようになりました。
関連リンク
- Go Issue #3927:
range
over string should treat surrogate halves as errors - Go CL 6458099:
all: make Unicode surrogate halves illegal as UTF-8
参考にした情報源リンク
- Unicode Consortium: The Unicode Standard
- https://www.unicode.org/versions/Unicode15.0.0/ch03.pdf (Chapter 3: Conformance, Section 3.9, U+D800..U+DFFF)
- Wikipedia: UTF-8
- Wikipedia: UTF-16
- Go言語のドキュメント:
unicode/utf8
パッケージ - Go言語のドキュメント:
The Go Programming Language Specification
(Strings, runes, and bytes) - Go言語のブログ:
Strings, bytes, runes and characters in Go
- Stack Overflow:
Why are surrogate pairs illegal in UTF-8?
RuneError
(U+FFFD) について