[インデックス 14678] ファイルの概要
このコミットは、Go言語の標準ライブラリ time パッケージにおける time.Parse 関数が time.StampNano レイアウトを使用した場合に発生するパニック(クラッシュ)を修正するものです。具体的には、time.StampNano が期待するナノ秒の桁数(9桁)と、入力文字列のナノ秒部分の桁数が一致しない場合に発生する問題に対処しています。
コミット
commit 2d3bdab0d61b9636e487ce4bc16429b8f0de8760
Author: Dave Cheney <dave@cheney.net>
Date: Tue Dec 18 07:52:23 2012 +1100
time: fix panic with time.Parse(time.StampNano, ... )
Fixes #4502.
R=rsc
CC=golang-dev
https://golang.org/cl/6949058
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2d3bdab0d61b9636e487ce4bc16429b8f0de8760
元コミット内容
time: fix panic with time.Parse(time.StampNano, ... )
このコミットは、time.Parse 関数が time.StampNano レイアウトと共に使用された際に発生するパニックを修正します。これはGoのIssue #4502として報告された問題に対応しています。
変更の背景
Go言語の time パッケージには、日付と時刻の文字列を Time 型にパースするための time.Parse 関数があります。この関数は、特定のレイアウト文字列に基づいて入力文字列を解析します。time.StampNano は、Jan _2 15:04:05.999999999 の形式を表す事前定義されたレイアウト定数です。このレイアウトは、秒の小数点以下に9桁のナノ秒を期待します。
しかし、Go Issue #4502で報告された問題は、time.Parse が time.StampNano レイアウトを使用し、かつ入力文字列のナノ秒部分が9桁に満たない場合にパニックを引き起こすというものでした。例えば、.0123 のように4桁のナノ秒が与えられた場合、time.Parse は9桁を期待するため、内部で配列の範囲外アクセスが発生し、パニックに至っていました。
この問題は、time.Parse の内部実装である parseNanoseconds 関数が、期待される桁数と実際の入力文字列の長さの不一致を適切に処理できていなかったことに起因します。特に、stdFracSecond0(固定桁数の小数秒を表す内部定数)の処理において、入力文字列の長さチェックが不十分でした。
前提知識の解説
time.Parse関数: Go言語のtimeパッケージで提供される関数で、指定されたレイアウト文字列と入力文字列に基づいてtime.Timeオブジェクトを生成します。- レイアウト文字列:
time.Parseが入力文字列を解析するために使用するフォーマット文字列です。Goでは、特定の参照時刻(Mon Jan 2 15:04:05 MST 2006)の各要素を対応するフォーマットコードとして使用します。 time.StampNano:timeパッケージで定義されている標準レイアウトの一つで、Jan _2 15:04:05.999999999の形式を表します。.999999999の部分はナノ秒(9桁)を示します。- パニック (Panic): Go言語におけるランタイムエラーの一種で、プログラムの実行を停止させます。通常、回復不可能なエラーやプログラマの論理的誤りによって発生します。このケースでは、配列の範囲外アクセスがパニックの原因でした。
stdFracSecond0:timeパッケージの内部で、固定桁数の小数秒をパースするために使用される定数です。レイアウト文字列中の.000や.000000000のような部分に対応します。parseNanoseconds関数:timeパッケージの内部関数で、文字列からナノ秒部分を解析し、整数値に変換する役割を担います。
技術的詳細
このコミットの主要な修正は、src/pkg/time/format.go 内の Parse 関数における stdFracSecond0 の処理と、parseNanoseconds 関数の呼び出し方にあります。
-
Parse関数内のstdFracSecond0処理の改善:- 以前のコードでは、
stdFracSecond0に対応するナノ秒の桁数ndigitを計算した後、直接parseNanosecondsを呼び出し、その結果に基づいてvalue文字列をスライスしていました。 - 修正後では、
parseNanosecondsを呼び出す前に、入力文字列valueの長さがndigit以上であるかをチェックするようになりました。 if len(value) < ndigitという条件が追加され、もし入力文字列が期待される桁数よりも短い場合、errBad(不正なフォーマットを示すエラー)を設定して処理を中断します。これにより、parseNanosecondsが不完全な文字列を処理しようとしてパニックになるのを防ぎます。
- 以前のコードでは、
-
parseNanoseconds関数の呼び出しとエラーハンドリングの改善:- 以前の
parseNanoseconds関数は、atoi(ASCII文字列を整数に変換する関数)の呼び出し結果を直接nsに代入し、その後のif err != nilでエラーをチェックしていました。 - 修正後では、
if ns, err = atoi(value[1:nbytes]); err != nilのように、atoiの結果をnsに代入すると同時にエラーチェックを行う、よりGoらしい簡潔な記述に変更されました。機能的な変更はありませんが、コードの可読性と慣用性が向上しています。
- 以前の
-
テストケースの追加:
src/pkg/time/time_test.goに、Issue #4502で報告されたシナリオを再現する新しいテストケースが追加されました。parseTests配列に、StampNanoレイアウトと、ナノ秒部分が9桁に満たない(例:.012345678)または9桁を超える(例:.0123)入力文字列の組み合わせが追加されています。これにより、修正が正しく機能することを確認できます。parseErrorTests配列にも、StampNanoが厳密に9桁の精度を要求することを示すエラーケース(例:.000000のような6桁の入力)と、余分なテキストがある場合のエラーケースが追加されました。
これらの変更により、time.Parse は time.StampNano のような固定桁数の小数秒を期待するレイアウトに対して、入力文字列の長さが不足している場合にパニックを起こす代わりに、適切なエラーを返すようになりました。
コアとなるコードの変更箇所
src/pkg/time/format.go
--- a/src/pkg/time/format.go
+++ b/src/pkg/time/format.go
@@ -854,9 +854,15 @@ func Parse(layout, value string) (Time, error) {
zoneName = p
case stdFracSecond0:
- ndigit := std >> stdArgShift
- nsec, rangeErrString, err = parseNanoseconds(value, 1+ndigit)
- value = value[1+ndigit:]
+ // stdFracSecond0 requires the exact number of digits as specified in
+ // the layout.
+ ndigit := 1 + (std >> stdArgShift)
+ if len(value) < ndigit {
+ err = errBad
+ break
+ }
+ nsec, rangeErrString, err = parseNanoseconds(value, ndigit)
+ value = value[ndigit:]
case stdFracSecond9:
if len(value) < 2 || value[0] != '.' || value[1] < '0' || '9' < value[1] {
@@ -934,8 +940,7 @@ func parseNanoseconds(value string, nbytes int) (ns int, rangeErrString string,
err = errBad
return
}
- ns, err = atoi(value[1:nbytes])
- if err != nil {
+ if ns, err = atoi(value[1:nbytes]); err != nil {
return
}
if ns < 0 || 1e9 <= ns {
src/pkg/time/time_test.go
--- a/src/pkg/time/time_test.go
+++ b/src/pkg/time/time_test.go
@@ -469,7 +469,7 @@ type ParseTest struct {
value string
hasTZ bool // contains a time zone
hasWD bool // contains a weekday
- yearSign int // sign of year
+ yearSign int // sign of year, -1 indicates the year is not present in the format
fracDigits int // number of digits of fractional second
}
@@ -514,6 +514,13 @@ var parseTests = []ParseTest{
{"", "2006-01-02 15:04:05.999999999 -0700 MST", "2010-02-04 21:00:57.0123 -0800 PST", true, false, 1, 4},
{"", "2006-01-02 15:04:05.9999 -0700 MST", "2010-02-04 21:00:57.012345678 -0800 PST", true, false, 1, 9},
{"", "2006-01-02 15:04:05.999999999 -0700 MST", "2010-02-04 21:00:57.012345678 -0800 PST", true, false, 1, 9},
+
+ // issue 4502.
+ {"", StampNano, "Feb 4 21:00:57.012345678", false, false, -1, 9},
+ {"", "Jan _2 15:04:05.999", "Feb 4 21:00:57.012300000", false, false, -1, 4},
+ {"", "Jan _2 15:04:05.999", "Feb 4 21:00:57.012345678", false, false, -1, 9},
+ {"", "Jan _2 15:04:05.999999999", "Feb 4 21:00:57.0123", false, false, -1, 4},
+ {"", "Jan _2 15:04:05.999999999", "Feb 4 21:00:57.012345678", false, false, -1, 9},
}
func TestParse(t *testing.T) {
@@ -549,7 +556,7 @@ func TestRubyParse(t *testing.T) {
func checkTime(time Time, test *ParseTest, t *testing.T) {
// The time should be Thu Feb 4 21:00:57 PST 2010
- if test.yearSign*time.Year() != 2010 {
+ if test.yearSign >= 0 && test.yearSign*time.Year() != 2010 {
t.Errorf("%s: bad year: %d not %d", test.name, time.Year(), 2010)
}
if time.Month() != February {
@@ -630,6 +637,9 @@ var parseErrorTests = []ParseErrorTest{
{"Mon Jan _2 15:04:05.000 2006", "Thu Feb 4 23:00:59x01 2010", "cannot parse"},
{"Mon Jan _2 15:04:05.000 2006", "Thu Feb 4 23:00:59.xxx 2010", "cannot parse"},
{"Mon Jan _2 15:04:05.000 2006", "Thu Feb 4 23:00:59.-123 2010", "fractional second out of range"},
+ // issue 4502. StampNano requires exactly 9 digits of precision.
+ {StampNano, "Dec 7 11:22:01.000000", `cannot parse ".000000" as ".000000000"`},
+ {StampNano, "Dec 7 11:22:01.0000000000", "extra text: 0"},
}
func TestParseErrors(t *testing.T) {
コアとなるコードの解説
src/pkg/time/format.go の変更
-
case stdFracSecond0:ブロック:ndigit := 1 + (std >> stdArgShift): ここで、stdFracSecond0レイアウトが期待する小数秒の桁数を計算しています。stdArgShiftは、レイアウト定数に埋め込まれた桁数情報を抽出するためのビットシフトです。1 +は、小数点の.を含めた全体の長さを考慮するためです。if len(value) < ndigit: この行が追加された最も重要な変更点です。valueはパース対象の文字列の残りの部分です。このチェックにより、入力文字列の残りの長さが、stdFracSecond0が期待する小数秒の桁数(小数点を含む)よりも短い場合に、パニックを回避します。err = errBad; break: 上記の条件が真の場合、つまり入力文字列が短すぎる場合は、errBadというエラーを設定し、現在のswitch文から抜けます。これにより、不完全な文字列をparseNanosecondsに渡すことを防ぎ、パニックを回避します。nsec, rangeErrString, err = parseNanoseconds(value, ndigit):parseNanoseconds関数は、指定されたndigitの長さでvalueからナノ秒をパースします。この呼び出しは、上記の長さチェックの後に行われるため、安全性が確保されます。value = value[ndigit:]: パースが成功した場合、valueをパース済みのナノ秒部分の長さだけ進めます。
-
parseNanoseconds関数内の変更:if ns, err = atoi(value[1:nbytes]); err != nil: この変更は、atoiの呼び出しとエラーチェックを1行にまとめたもので、Goの慣用的な書き方です。機能的な変更はありませんが、コードがより簡潔になりました。value[1:nbytes]は、小数点の.を除いたナノ秒の数字部分をatoiに渡しています。
src/pkg/time/time_test.go の変更
-
ParseTest構造体:yearSign int // sign of year, -1 indicates the year is not present in the format:yearSignフィールドのコメントが更新され、-1が年がフォーマットに存在しないことを示すという説明が追加されました。これは、新しいテストケースで年が指定されない場合にyearSignを-1に設定するためです。
-
parseTests配列への追加:// issue 4502.のコメントの下に、time.StampNanoレイアウトと様々なナノ秒の長さを持つ入力文字列の組み合わせが追加されました。これらは、time.Parseがパニックを起こさずに正しくパースできることを検証するためのものです。特に、ナノ秒の桁数が9桁に満たない場合や、9桁を超える場合(ただし、StampNanoは厳密に9桁を期待するため、これはエラーになるべきケース)の挙動をテストしています。
-
checkTime関数内の変更:if test.yearSign >= 0 && test.yearSign*time.Year() != 2010:yearSignが-1の場合は年のチェックをスキップするように条件が追加されました。これにより、年がフォーマットに含まれないテストケースでもcheckTimeが正しく動作するようになります。
-
parseErrorTests配列への追加:// issue 4502. StampNano requires exactly 9 digits of precision.のコメントの下に、time.StampNanoが厳密に9桁の精度を要求することを示すエラーケースが追加されました。{StampNano, "Dec 7 11:22:01.000000",cannot parse ".000000" as ".000000000"}: このテストケースは、StampNanoが9桁のナノ秒を期待するにもかかわらず、6桁のナノ秒が与えられた場合に、期待されるエラーメッセージが返されることを検証します。{StampNano, "Dec 7 11:22:01.0000000000", "extra text: 0"}: このテストケースは、9桁を超えるナノ秒が与えられた場合に、余分なテキストとして認識され、適切なエラーが返されることを検証します。
これらの変更により、time.Parse の堅牢性が向上し、特に固定桁数の小数秒を扱う際のパニックが回避されるようになりました。
関連リンク
- Go Issue #4502: https://github.com/golang/go/issues/4502 (このコミットが修正した問題の報告)
- Go CL 6949058: https://golang.org/cl/6949058 (このコミットに対応するGoのコードレビュー)
参考にした情報源リンク
- Go言語の
timeパッケージに関する公式ドキュメント: https://pkg.go.dev/time - Go言語の
time.Parse関数の使い方に関する一般的な情報源 (例: Go by Example, A Tour of Goなど) - Go言語におけるパニックとエラーハンドリングに関する情報