[インデックス 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言語におけるパニックとエラーハンドリングに関する情報