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

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

このコミットは、Go言語の標準ライブラリstrconvパッケージ内のCanBackquote関数の振る舞いを修正し、無効なUTF-8シーケンスを含む文字列に対して正しくfalseを返すように変更します。また、バイトオーダーマーク(BOM)を含む文字列に対するCanBackquoteの挙動を示すテストが追加されています。

変更されたファイルは以下の通りです。

  • src/pkg/strconv/quote.go: CanBackquote関数の実装が変更されました。
  • src/pkg/strconv/quote_test.go: CanBackquote関数のテストケースが追加されました。

コミット

strconv: fix CanBackquote for invalid UTF-8

Make CanBackquote(invalid UTF-8) return false.

Also add two test which show that CanBackquote reports
true for strings containing a BOM.

Fixes #7572.

LGTM=r
R=golang-codereviews, bradfitz, r
CC=golang-codereviews
https://golang.org/cl/111780045

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

https://github.com/golang/go/commit/c0a824aad69f8eeb15868675c47d3ceb16277576

元コミット内容

commit c0a824aad69f8eeb15868675c47d3ceb16277576
Author: Volker Dobler <dr.volker.dobler@gmail.com>
Date:   Mon Jul 14 19:49:26 2014 -0700

    strconv: fix CanBackquote for invalid UTF-8
    
    Make CanBackquote(invalid UTF-8) return false.
    
    Also add two test which show that CanBackquote reports
    true for strings containing a BOM.
    
    Fixes #7572.
    
    LGTM=r
    R=golang-codereviews, bradfitz, r
    CC=golang-codereviews
    https://golang.org/cl/111780045
---
 src/pkg/strconv/quote.go      | 13 ++++++++++---\n src/pkg/strconv/quote_test.go |  4 ++++\n 2 files changed, 14 insertions(+), 3 deletions(-)\n
diff --git a/src/pkg/strconv/quote.go b/src/pkg/strconv/quote.go
index aded7e5930..89dda99750 100644
--- a/src/pkg/strconv/quote.go
+++ b/src/pkg/strconv/quote.go
@@ -143,9 +143,16 @@ func AppendQuoteRuneToASCII(dst []byte, r rune) []byte {
 // unchanged as a single-line backquoted string without control
 // characters other than space and tab.
 func CanBackquote(s string) bool {
-\tfor i := 0; i < len(s); i++ {\n-\t\tc := s[i]\n-\t\tif (c < \' \' && c != \'\\t\') || c == \'`\' || c == \'\\u007F\' {\n+\tfor len(s) > 0 {\n+\t\tr, wid := utf8.DecodeRuneInString(s)\n+\t\ts = s[wid:]\n+\t\tif wid > 1 {\n+\t\t\tcontinue // All multibyte runes are correctly encoded and assumed printable.\n+\t\t}\n+\t\tif r == utf8.RuneError {\n+\t\t\treturn false\n+\t\t}\n+\t\tif (r < \' \' && r != \'\\t\') || r == \'`\' || r == \'\\u007F\' {\n \t\t\treturn false\n \t\t}\n \t}\ndiff --git a/src/pkg/strconv/quote_test.go b/src/pkg/strconv/quote_test.go
index e4b5b6b9fd..24998191d7 100644
--- a/src/pkg/strconv/quote_test.go
+++ b/src/pkg/strconv/quote_test.go
@@ -146,6 +146,10 @@ var canbackquotetests = []canBackquoteTest{\n \t{`ABCDEFGHIJKLMNOPQRSTUVWXYZ`, true},\n \t{`abcdefghijklmnopqrstuvwxyz`, true},\n \t{`☺`, true},\n+\t{\"\\x80\", false},\n+\t{\"a\\xe0\\xa0z\", false},\n+\t{\"\\ufeffabc\", true},\n+\t{\"a\\ufeffz\", true},\n }\n \n func TestCanBackquote(t *testing.T) {\n```

## 変更の背景

このコミットの背景には、Go言語の`strconv`パッケージにある`CanBackquote`関数が、無効なUTF-8シーケンスを含む文字列に対して誤った結果を返す可能性があったという問題があります。具体的には、Goの文字列はバイトのシーケンスであり、通常はUTF-8として扱われますが、`CanBackquote`関数は以前、文字列をバイト単位で走査し、ASCII範囲の特定の制御文字やバッククォート文字、DEL文字(`\u007F`)をチェックしていました。このアプローチでは、無効なUTF-8バイトシーケンスが検出されず、それらが有効なバッククォート文字列として誤って認識される可能性がありました。

Go言語のバッククォート文字列リテラル(raw string literal)は、エスケープシーケンスを解釈せず、文字列をそのまま表現するために使用されます。そのため、バッククォート文字列として表現できる文字列は、制御文字(タブを除く)、バッククォート文字、DEL文字を含まない必要があります。無効なUTF-8シーケンスは、Goの文字列の性質上、そのままバッククォート文字列として表現されるべきではありません。

この問題は、Go issue #7572として報告されており、このコミットはその問題を修正することを目的としています。

## 前提知識の解説

### `strconv`パッケージ

`strconv`パッケージは、Go言語の標準ライブラリの一部であり、基本的なデータ型(整数、浮動小数点数、ブール値など)と文字列との間の変換機能を提供します。例えば、`Atoi`(文字列を整数に変換)、`Itoa`(整数を文字列に変換)、`ParseBool`(文字列をブール値に変換)などの関数が含まれています。

### `CanBackquote`関数

`strconv.CanBackquote(s string) bool`関数は、与えられた文字列`s`が、Goのバッククォート文字列リテラルとして、エスケープなしで単一行で表現できるかどうかを判定します。バッククォート文字列リテラルは、` ` `で囲まれた文字列で、内部のエスケープシーケンスが解釈されません。この関数が`true`を返すのは、文字列が以下の条件を満たす場合です。

- 制御文字(`\t`タブを除く)を含まない。
- バッククォート文字(` ` `)を含まない。
- DEL文字(`\u007F`)を含まない。
- (このコミットの変更後)無効なUTF-8シーケンスを含まない。

### UTF-8

UTF-8(Unicode Transformation Format - 8-bit)は、Unicode文字を可変長バイトシーケンスでエンコードするための文字エンコーディング方式です。ASCII文字は1バイトで表現され、非ASCII文字は2バイト以上で表現されます。UTF-8は、インターネット上で最も広く使用されている文字エンコーディングです。Go言語の文字列は、内部的にはバイトのシーケンスとして扱われますが、Goの標準ライブラリのほとんどの関数は、これらのバイトシーケンスがUTF-8エンコードされたテキストを表していると期待します。

### BOM (Byte Order Mark)

BOM(Byte Order Mark)は、Unicodeテキストファイルの先頭に置かれる特別なバイトシーケンスで、ファイルのエンコーディング(UTF-8, UTF-16, UTF-32など)とバイト順(エンディアン)を示すために使用されます。UTF-8の場合、BOMは`EF BB BF`という3バイトのシーケンスで表現されます。UTF-8ではバイト順の概念がないため、BOMは厳密には不要ですが、一部のテキストエディタやシステムが互換性のために付加することがあります。Go言語では、BOMは通常のUnicode文字`U+FEFF`(ZERO WIDTH NO-BREAK SPACE)として扱われます。

### Go言語における文字列の扱いと`utf8.DecodeRuneInString`

Go言語の文字列は、不変のバイトスライス(`[]byte`)として実装されています。これは、C言語のようなヌル終端文字列や、JavaのようなUTF-16エンコードされた文字のシーケンスとは異なります。Goの文字列は、UTF-8エンコードされたテキストを保持することを意図していますが、Goランタイムは文字列の内容が常に有効なUTF-8であるとは限りません。無効なUTF-8シーケンスが含まれていても、Goはエラーを発生させずにそのままバイトスライスとして扱います。

`unicode/utf8`パッケージは、UTF-8エンコードされたバイトスライスを操作するための関数を提供します。その中でも`utf8.DecodeRuneInString(s string) (r rune, size int)`関数は重要です。この関数は、文字列`s`の先頭から1つのUTF-8エンコードされたUnicodeコードポイント(`rune`型)をデコードし、そのルーンと、デコードに使用されたバイト数(`size`)を返します。もし文字列の先頭が無効なUTF-8シーケンスである場合、`utf8.DecodeRuneInString`は`utf8.RuneError`(U+FFFD、Unicodeの「Replacement Character」)と、その無効なシーケンスのバイト数(通常は1)を返します。

## 技術的詳細

このコミットの主要な変更点は、`strconv.CanBackquote`関数が文字列を走査する方法にあります。

**変更前の`CanBackquote`のロジック:**

```go
func CanBackquote(s string) bool {
	for i := 0; i < len(s); i++ {
		c := s[i]
		if (c < ' ' && c != '\t') || c == '`' || c == '\u007F' {
			return false
		}
	}
	return true
}

変更前は、文字列をバイト単位で走査し、各バイトcがASCII範囲の制御文字(タブを除く)、バッククォート文字、またはDEL文字であるかをチェックしていました。このアプローチでは、マルチバイト文字(UTF-8エンコードされた非ASCII文字)や無効なUTF-8シーケンスを正しく処理できませんでした。例えば、無効なUTF-8バイトシーケンスがASCII範囲の制御文字ではない場合、この関数はtrueを返してしまう可能性がありました。

変更後のCanBackquoteのロジック:

func CanBackquote(s string) bool {
	for len(s) > 0 {
		r, wid := utf8.DecodeRuneInString(s)
		s = s[wid:]
		if wid > 1 {
			continue // All multibyte runes are correctly encoded and assumed printable.
		}
		if r == utf8.RuneError {
			return false
		}
		if (r < ' ' && r != '\t') || r == '`' || r == '\u007F' {
			return false
		}
	}
	return true
}

変更後では、文字列をバイト単位ではなく、UTF-8エンコードされたルーン(Unicodeコードポイント)単位で走査するように変更されました。

  1. for len(s) > 0 ループで、文字列sが空になるまで処理を続けます。
  2. r, wid := utf8.DecodeRuneInString(s): sの先頭から1つのルーンrと、そのルーンが占めるバイト数widをデコードします。
  3. s = s[wid:]: デコードしたルーンの分だけ文字列sをスライスし、次のルーンの処理に進みます。
  4. if wid > 1 { continue }: もしデコードされたルーンがマルチバイト文字(wid > 1)であれば、それは有効なUTF-8エンコードされた文字であり、かつ通常は印字可能であると仮定して、特別なチェックなしに次のルーンの処理に進みます。これは、マルチバイト文字が制御文字やバッククォート文字である可能性が低いという前提に基づいています。
  5. if r == utf8.RuneError { return false }: ここが重要な変更点です。もしutf8.DecodeRuneInStringが無効なUTF-8シーケンスを検出した場合、rutf8.RuneError(U+FFFD)を返します。この場合、文字列は無効なUTF-8を含んでいるため、CanBackquotefalseを返します。
  6. if (r < ' ' && r != '\t') || r == '' || r == '\u007F' { return false }: デコードされたルーンrが、ASCII範囲の制御文字(タブを除く)、バッククォート文字、またはDEL文字であるかをチェックします。これらの文字が含まれていれば、CanBackquotefalse`を返します。

この変更により、CanBackquoteは無効なUTF-8シーケンスを正しく検出し、バッククォート文字列として不適切であると判断できるようになりました。

また、テストケースとして以下の2つが追加されました。

  • {"\x80", false}: 単一の無効なUTF-8バイト(\x80はUTF-8の開始バイトとしては無効)を含む文字列がfalseを返すことを確認します。
  • {"a\xe0\xa0z", false}: 無効なUTF-8シーケンス(\xe0\xa0は不完全なUTF-8シーケンス)を含む文字列がfalseを返すことを確認します。
  • {"\ufeffabc", true}: UTF-8 BOM(\ufeffはU+FEFF)を含む文字列がtrueを返すことを確認します。これは、BOMがGoでは通常のUnicode文字として扱われ、制御文字やバッククォート文字ではないため、バッククォート文字列として表現可能であるという挙動を示しています。
  • {"a\ufeffz", true}: 文字列の途中にBOMを含む場合も同様にtrueを返すことを確認します。

これらのテストケースは、CanBackquoteが有効なUTF-8文字(BOMを含む)と無効なUTF-8シーケンスを区別して処理できることを保証します。

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

src/pkg/strconv/quote.goCanBackquote 関数:

--- a/src/pkg/strconv/quote.go
+++ b/src/pkg/strconv/quote.go
@@ -143,9 +143,16 @@ func AppendQuoteRuneToASCII(dst []byte, r rune) []byte {
 // unchanged as a single-line backquoted string without control
 // characters other than space and tab.
 func CanBackquote(s string) bool {
-\tfor i := 0; i < len(s); i++ {\n-\t\tc := s[i]\n-\t\tif (c < \' \' && c != \'\\t\') || c == \'`\' || c == \'\\u007F\' {\n+\tfor len(s) > 0 {\n+\t\tr, wid := utf8.DecodeRuneInString(s)\n+\t\ts = s[wid:]\n+\t\tif wid > 1 {\n+\t\t\tcontinue // All multibyte runes are correctly encoded and assumed printable.\n+\t\t}\n+\t\tif r == utf8.RuneError {\n+\t\t\treturn false\n+\t\t}\n+\t\tif (r < \' \' && r != \'\\t\') || r == \'`\' || r == \'\\u007F\' {\n \t\t\treturn false
 \t\t}\
 \t}

src/pkg/strconv/quote_test.gocanbackquotetests 変数:

--- a/src/pkg/strconv/quote_test.go
+++ b/src/pkg/strconv/quote_test.go
@@ -146,6 +146,10 @@ var canbackquotetests = []canBackquoteTest{\n \t{`ABCDEFGHIJKLMNOPQRSTUVWXYZ`, true},\n \t{`abcdefghijklmnopqrstuvwxyz`, true},\n \t{`☺`, true},\n+\t{\"\\x80\", false},\n+\t{\"a\\xe0\\xa0z\", false},\n+\t{\"\\ufeffabc\", true},\n+\t{\"a\\ufeffz\", true},\n }\n \n func TestCanBackquote(t *testing.T) {\n```

## コアとなるコードの解説

### `src/pkg/strconv/quote.go` の `CanBackquote` 関数

- **変更前**:
    - `for i := 0; i < len(s); i++ { c := s[i] ... }`: 文字列をバイト単位でループしていました。`c`は単一のバイトでした。
    - `if (c < ' ' && c != '\t') || c == '`' || c == '\u007F'`: 各バイトが制御文字、バッククォート、またはDEL文字であるかをチェックしていました。このチェックはASCII文字には有効でしたが、マルチバイト文字や無効なUTF-8シーケンスに対しては不十分でした。

- **変更後**:
    - `for len(s) > 0 { ... }`: 文字列が空になるまでループを続けます。
    - `r, wid := utf8.DecodeRuneInString(s)`: `s`の先頭からUTF-8エンコードされたルーンをデコードします。`r`はデコードされたルーン(Unicodeコードポイント)、`wid`はそのルーンが占めるバイト数です。
    - `s = s[wid:]`: デコードしたルーンのバイト数分だけ文字列`s`をスライスし、次のイテレーションでは残りの文字列を処理します。
    - `if wid > 1 { continue }`: もし`wid`が1より大きい場合(つまり、マルチバイト文字の場合)、そのルーンは有効なUTF-8エンコードされた文字であり、通常は印字可能であると見なされます。そのため、追加のチェックなしに次のルーンに進みます。
    - `if r == utf8.RuneError { return false }`: **この行が最も重要な変更点です。** `utf8.DecodeRuneInString`が`utf8.RuneError`を返した場合、それは入力文字列が無効なUTF-8シーケンスを含んでいることを意味します。このような文字列はバッククォート文字列として適切ではないため、直ちに`false`を返します。
    - `if (r < ' ' && r != '\t') || r == '`' || r == '\u007F' { return false }`: デコードされたルーン`r`が、ASCII範囲の制御文字(タブを除く)、バッククォート文字、またはDEL文字であるかをチェックします。これらの文字が含まれていれば、`false`を返します。このチェックは、バイト単位からルーン単位に変わったことで、より正確になりました。

### `src/pkg/strconv/quote_test.go` の `canbackquotetests` 変数

- `{"\x80", false}`: `\x80`は単一のバイトで、UTF-8の開始バイトとしては無効です。このテストは、無効なUTF-8バイトを含む文字列が`CanBackquote`によって`false`と評価されることを確認します。
- `{"a\xe0\xa0z", false}`: `\xe0\xa0`は不完全なUTF-8シーケンス(3バイト文字の最初の2バイト)です。このテストは、無効なUTF-8シーケンスを含む文字列が`CanBackquote`によって`false`と評価されることを確認します。
- `{"\ufeffabc", true}`: `\ufeff`はUTF-8 BOM(U+FEFF)を表します。Goではこれは通常のUnicode文字として扱われます。このテストは、BOMを含む文字列が`CanBackquote`によって`true`と評価されることを確認します。これは、BOMが制御文字やバッククォート文字ではないため、バッククォート文字列として表現可能であるというGoの挙動を示しています。
- `{"a\ufeffz", true}`: 文字列の途中にBOMが含まれる場合も同様に`true`と評価されることを確認します。

これらのテストケースは、`CanBackquote`関数が、無効なUTF-8シーケンスを正しく識別し、一方でBOMのような有効な(しかし特殊な)Unicode文字を含む文字列も適切に処理できることを保証します。

## 関連リンク

- **Go issue #7572**: [https://github.com/golang/go/issues/7572](https://github.com/golang/go/issues/7572)
- **Go CL 111780045**: [https://golang.org/cl/111780045](https://golang.org/cl/111780045)

## 参考にした情報源リンク

- Go言語公式ドキュメント: `strconv`パッケージ ([https://pkg.go.dev/strconv](https://pkg.go.dev/strconv))
- Go言語公式ドキュメント: `unicode/utf8`パッケージ ([https://pkg.go.dev/unicode/utf8](https://pkg.go.dev/unicode/utf8))
- Go言語の文字列について ([https://go.dev/blog/strings](https://go.dev/blog/strings))
- UTF-8とBOMに関する一般的な情報 (例: Wikipediaなど)
- Goのバッククォート文字列リテラルに関する情報 (Go言語仕様など)

I have generated the detailed explanation in Markdown format, following all the instructions and chapter structure. I have included background, prerequisite knowledge, technical details, code changes, and relevant links. I also used the web search results to enrich the "前提知識の解説" and "技術的詳細" sections.