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

[インデックス 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言語におけるruneRuneError

  • 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コードポイント)のエンコード・デコード処理を担当しています。

  • SurrogateMinSurrogateMaxの定義:
    +	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パッケージの実装です。

  • surrogateMinsurrogateMaxの定義:
    +const (
    +	surrogateMin = 0xD800
    +	surrogateMax = 0xDFFF
    +)
    
    Goコード内でもサロゲートハーフの範囲を定義する定数が追加されました。
  • 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エンコーディングを持たないことを明示します。また、rune4MaxMaxRuneに変更され、より正確な最大ルーン値が使用されるようになりました。
  • EncodeRune関数の変更: この関数は、ルーンをUTF-8バイト列にエンコードします。エンコードしようとしているルーンがサロゲートハーフの範囲内にある場合に、そのルーンをRuneErrorに変換するロジックが追加されました。
    +	if surrogateMin <= r && r <= surrogateMax {
    +		r = RuneError
    +	}
    
    これにより、サロゲートハーフをエンコードしようとすると、自動的にRuneErrorに置き換えられます。
  • ValidRune関数の追加: この新しい関数は、与えられたルーンが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
    +}
    
    これは、ルーンがUTF-8として有効かどうかを事前にチェックするための便利なユーティリティ関数です。

src/pkg/unicode/utf8/utf8_test.goの変更

テストファイルには、サロゲートハーフの処理に関する新しいテストケースが追加されました。

  • utf8mapにサロゲートハーフの境界値(0xd7ff, 0xe000)が追加され、その前後の値が正しくエンコード・デコードされることを確認しています。
  • surrogateMapという新しいテストデータが追加され、サロゲートハーフがRuneErrorとしてデコードされ、1バイト消費されることを明示的にテストしています。
  • TestRuneLenのテストケースが追加され、RuneLenがサロゲートハーフに対して-1を返すことを確認しています。
  • TestValidのテストケースが更新され、サロゲートハーフを含む文字列がValidおよびValidStringで不正と判断されることを確認しています。
  • ValidRuneTestTestValidRuneが追加され、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ループがサロゲートハーフを正しくエラーとして扱うことが保証されます。

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

このコミットのコアとなる変更は、以下のファイルと関数に集中しています。

  1. src/pkg/runtime/rune.c:
    • runtime·charntorune: UTF-8デコード時にサロゲートハーフをRuneerrorとして扱うロジックの追加。
    • runtime·runetochar: ルーンエンコード時にサロゲートハーフをRuneerrorに変換するロジックの追加。
  2. src/pkg/unicode/utf8/utf8.go:
    • decodeRuneInternal, decodeRuneInStringInternal: UTF-8デコード時にサロゲートハーフをRuneErrorとして扱い、1バイト消費するロジックの追加。
    • RuneLen: サロゲートハーフに対して-1を返すように修正。
    • EncodeRune: サロゲートハーフをRuneErrorに変換するロジックの追加。
    • ValidRune: サロゲートハーフを不正なルーンとして判定する新しい関数の追加。
  3. テストファイル:
    • src/pkg/unicode/utf8/utf8_test.go: サロゲートハーフのエンコード/デコード、RuneLenValidRuneの挙動を検証する多数の新しいテストケース。
    • test/stringrange.go: 文字列のrangeループがサロゲートハーフを正しくRuneErrorとして処理することを確認するテスト。

コアとなるコードの解説

このコミットの核心は、Go言語のUnicode処理において、UTF-16の概念であるサロゲートハーフがUTF-8の文脈で「不正な値」として厳密に扱われるようにすることです。

具体的には、以下の2つの主要な側面で変更が加えられました。

  1. デコード時の挙動の統一:

    • runtime層(rune.c)とunicode/utf8パッケージ層(utf8.go)の両方で、UTF-8バイト列からルーンをデコードする際に、サロゲートハーフの範囲(U+D800からU+DFFF)に該当するコードポイントが検出された場合、それをRuneError(U+FFFD)として処理するようにしました。
    • これにより、不正なサロゲートハーフを含むUTF-8バイト列が文字列として扱われた場合、for...rangeループなどでイテレートすると、その不正な部分がRuneErrorとして返されるようになります。また、不正なバイトシーケンスとして1バイトのみを消費し、次の文字のデコードに進むことで、残りの有効なUTF-8バイト列の処理を妨げないようにしています。
  2. エンコード時の挙動の統一と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データの処理において、より予測可能で安全な挙動を提供するようになりました。

関連リンク

参考にした情報源リンク