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

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

このコミットは、Go言語の標準ライブラリである regexp パッケージにおける Replace 関数が、正規表現のサブマッチ($1 など)を置換文字列として使用する際に発生するパニック(プログラムの異常終了)を修正するものです。特に、サブマッチが存在しない場合や、正規表現がマッチしなかった場合に問題が発生していました。

コミット

commit 54b7ccd514f6a689347c8d1f876bec90613f28f8
Author: Erik St. Martin <alakriti@gmail.com>
Date:   Sat Dec 22 11:14:56 2012 -0500

          regexp: fix index panic in Replace
    
    When using subexpressions ($1) as replacements, when they either don't exist or values weren't found causes a panic.
    This patch ensures that the match location isn't -1, to prevent out of bounds errors.
    Fixes #3816.
    
    R=franciscossouza, rsc
    CC=golang-dev
    https://golang.org/cl/6931049

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

https://github.com/golang/go/commit/54b7ccd514f6a689347c8d1f876bec90613f28f8

元コミット内容

このコミットは、Go言語の正規表現パッケージ regexp における Replace 関数が、サブマッチ(例: $1)を置換文字列として利用する際に発生するパニックを修正します。具体的には、サブマッチが存在しない場合や、正規表現が入力文字列にマッチしなかった場合に、内部的なインデックスが不正な値(-1)となり、それが原因で配列の範囲外アクセス(out of bounds error)が発生し、パニックを引き起こしていました。

この修正は、match スライスから値を取得する際に、そのインデックスが有効であるか(-1ではないか)を明示的にチェックすることで、この問題を解決しています。

変更の背景

Go言語の regexp パッケージは、Perl互換の正規表現をサポートしており、ReplaceAllStringReplaceAllLiteralString といった関数を通じて、マッチした部分を別の文字列に置換する機能を提供しています。この置換文字列には、正規表現でキャプチャされたグループ(サブマッチ)を参照するための $1, $2 などの構文を使用できます。

しかし、正規表現がマッチしなかった場合や、オプションのグループ(例: (x)?)がマッチしなかった場合、対応するサブマッチのインデックスは内部的に -1 として表現されます。従来の Replace 関数の実装では、この -1 のインデックスが適切に処理されず、match スライスへのアクセス時に範囲外エラーが発生し、プログラムがパニックに陥る可能性がありました。

この問題は、Go Issue #3816として報告されており、このコミットはその問題を解決するために作成されました。ユーザーが予期しないパニックに遭遇することなく、より堅牢な正規表現置換機能を提供することが変更の背景にあります。

前提知識の解説

このコミットの理解には、以下の前提知識が役立ちます。

  • 正規表現 (Regular Expressions): 文字列のパターンを記述するための強力なツール。特定の文字の並びや、繰り返し、選択などを表現できます。
  • キャプチャリンググループ (Capturing Groups): 正規表現において、括弧 () で囲まれた部分。マッチした文字列の一部を「キャプチャ」し、後で参照できるようにします。これらは通常、$1, $2 などのバックリファレンスで参照されます。
  • Go言語の regexp パッケージ: Go言語標準ライブラリの一部で、正規表現のコンパイル、マッチング、置換などの機能を提供します。
  • Regexp.FindSubmatchIndex: このメソッドは、正規表現がマッチした部分と、すべてのキャプチャリンググループのマッチ位置をバイトインデックスのペアの配列として返します。例えば、match[0]match[1]は全体のマッチ範囲、match[2]match[3]は最初のキャプチャリンググループの範囲を示します。マッチしなかったグループやオプションのグループは、対応するインデックスが -1 となります。
  • ReplaceAllString / ReplaceAllLiteralString: regexp パッケージの関数で、正規表現にマッチした部分を置換文字列で置き換えます。ReplaceAllString は置換文字列内で $1 などのサブマッチ参照を解釈しますが、ReplaceAllLiteralString はリテラルとして扱います。
  • パニック (Panic): Go言語におけるランタイムエラーの一種。通常、回復不可能なエラーやプログラマーの論理的な誤りによって発生し、プログラムの実行を停止させます。今回のケースでは、配列の範囲外アクセスがパニックの原因でした。
  • expand メソッド: regexp パッケージの内部メソッドで、置換文字列のテンプレート($1 などを含む)を実際の文字列に展開する役割を担っています。このメソッド内で、match スライスからサブマッチのバイト範囲を取得し、元の文字列からその部分を抽出します。

技術的詳細

この修正の核心は、src/pkg/regexp/regexp.go ファイル内の expand メソッドにあります。このメソッドは、正規表現のマッチ結果(match スライス)と置換テンプレート(template)を受け取り、最終的な置換文字列を構築します。

match スライスは、正規表現がマッチした各部分の開始インデックスと終了インデックスのペアを格納しています。例えば、match[2*num]num 番目のキャプチャリンググループの開始インデックス、match[2*num+1] はその終了インデックスを示します。

問題は、キャプチャリンググループがマッチしなかった場合(例: (x)?"" にマッチした場合や、パターン全体がマッチしなかった場合)に、対応する match スライスの要素が -1 になることです。従来のコードでは、2*num+1 < len(match) という条件のみで match スライスへのアクセスを許可していました。しかし、この条件だけでは、match[2*num]-1 であっても条件を満たしてしまう可能性がありました。

例えば、match スライスが [0, 3, -1, -1] のような状態(全体が0-3でマッチし、最初のキャプチャリンググループがマッチしなかった)の場合、num=1(2番目のキャプチャリンググループ)を処理しようとすると、2*num22*num+13 となります。2*num+1 < len(match)3 < 4true となりますが、match[2]-1 です。この -1 をバイトスライス bsrc のインデックスとして使用しようとすると、負のインデックスアクセスとなり、Goランタイムがパニックを引き起こします。

修正では、この問題に対処するため、以下の条件が追加されました。

if 2*num+1 < len(match) && match[2*num] >= 0 {

新しい条件 && match[2*num] >= 0 は、num 番目のキャプチャリンググループの開始インデックスが有効な値(0以上)であることを保証します。これにより、-1 のインデックスが bsrc スライスへのアクセスに使用されることを防ぎ、パニックを回避します。

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

変更は主に以下のファイルと行に集中しています。

src/pkg/regexp/regexp.go

--- a/src/pkg/regexp/regexp.go
+++ b/src/pkg/regexp/regexp.go
@@ -767,7 +767,7 @@ func (re *Regexp) expand(dst []byte, template string, bsrc []byte, src string, m
 		}
 		template = rest
 		if num >= 0 {
-			if 2*num+1 < len(match) {
+			if 2*num+1 < len(match) && match[2*num] >= 0 {
 				if bsrc != nil {
 					dst = append(dst, bsrc[match[2*num]:match[2*num+1]]...)
 				} else {

src/pkg/regexp/all_test.go

--- a/src/pkg/regexp/all_test.go
+++ b/src/pkg/regexp/all_test.go
@@ -196,6 +196,10 @@ var replaceTests = []ReplaceTest{
 	{"a+", "${oops", "aaa", "${oops"},
 	{"a+", "$$", "aaa", "$"},
 	{"a+", "$", "aaa", "$"},
+
+	// Substitution when subexpression isn't found
+	{"(x)?", "$1", "123", "123"},
+	{"abc", "$1", "123", "123"},
 }

コアとなるコードの解説

src/pkg/regexp/regexp.go の変更

expand 関数は、正規表現の置換処理において、テンプレート文字列(例: "$1")を実際のマッチした部分文字列に展開する役割を担っています。

  • num は、現在処理しているキャプチャリンググループのインデックス(例: $1 なら num=1)。
  • match は、Regexp.FindSubmatchIndex などから返される、マッチした部分文字列の開始/終了バイトインデックスのペアの配列です。match[2*num]num 番目のグループの開始インデックス、match[2*num+1] は終了インデックスです。

変更前のコード if 2*num+1 < len(match) は、match スライスが num 番目のキャプチャリンググループのインデックスペアを格納するのに十分な長さがあるか、という点のみをチェックしていました。しかし、前述の通り、match[2*num]match[2*num+1] の値自体が -1 である可能性を考慮していませんでした。

追加された条件 && match[2*num] >= 0 は、num 番目のキャプチャリンググループの開始インデックスが実際に有効な(非負の)インデックスであることを保証します。これにより、bsrc[match[2*num]:match[2*num+1]] のようなスライス操作が、負のインデックスや不正な範囲で実行されることを防ぎ、パニックを回避します。

src/pkg/regexp/all_test.go の変更

テストファイルには、この修正が正しく機能することを確認するための新しいテストケースが追加されています。

  • {"(x)?", "$1", "123", "123"}:

    • 正規表現 (x)? は、x が0回または1回出現することにマッチします。
    • 入力文字列 123 には x が含まれていないため、(x)? は空文字列にマッチし、キャプチャリンググループ $1 はマッチしません(内部的には -1 となる)。
    • 期待される結果は 123 です。これは、$1 が空文字列として扱われ、元の文字列がそのまま返されることを意味します。このテストは、サブマッチが見つからない場合にパニックが発生しないことを確認します。
  • {"abc", "$1", "123", "123"}:

    • 正規表現 abc は、入力文字列 123 にはマッチしません。
    • この場合、$1 は存在しないため、置換は行われず、元の文字列 123 がそのまま返されるべきです。
    • このテストも、正規表現が全くマッチしない場合に $1 の参照がパニックを引き起こさないことを確認します。

これらのテストケースは、修正が意図した通りに、サブマッチが存在しない、または正規表現がマッチしない状況での Replace 関数の堅牢性を向上させていることを検証します。

関連リンク

参考にした情報源リンク

  • Go言語 regexp パッケージのドキュメント: https://pkg.go.dev/regexp
  • Go言語の正規表現に関するブログ記事やチュートリアル (一般的な知識として)
  • Go言語のパニックと回復に関するドキュメント (一般的な知識として)
  • Gitのコミットと差分表示に関する一般的な情報 (一般的な知識として)