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

[インデックス 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.Parsetime.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 関数の呼び出し方にあります。

  1. Parse 関数内の stdFracSecond0 処理の改善:

    • 以前のコードでは、stdFracSecond0 に対応するナノ秒の桁数 ndigit を計算した後、直接 parseNanoseconds を呼び出し、その結果に基づいて value 文字列をスライスしていました。
    • 修正後では、parseNanoseconds を呼び出す前に、入力文字列 value の長さが ndigit 以上であるかをチェックするようになりました。
    • if len(value) < ndigit という条件が追加され、もし入力文字列が期待される桁数よりも短い場合、errBad(不正なフォーマットを示すエラー)を設定して処理を中断します。これにより、parseNanoseconds が不完全な文字列を処理しようとしてパニックになるのを防ぎます。
  2. parseNanoseconds 関数の呼び出しとエラーハンドリングの改善:

    • 以前の parseNanoseconds 関数は、atoi(ASCII文字列を整数に変換する関数)の呼び出し結果を直接 ns に代入し、その後の if err != nil でエラーをチェックしていました。
    • 修正後では、if ns, err = atoi(value[1:nbytes]); err != nil のように、atoi の結果を ns に代入すると同時にエラーチェックを行う、よりGoらしい簡潔な記述に変更されました。機能的な変更はありませんが、コードの可読性と慣用性が向上しています。
  3. テストケースの追加:

    • src/pkg/time/time_test.go に、Issue #4502で報告されたシナリオを再現する新しいテストケースが追加されました。
    • parseTests 配列に、StampNano レイアウトと、ナノ秒部分が9桁に満たない(例: .012345678)または9桁を超える(例: .0123)入力文字列の組み合わせが追加されています。これにより、修正が正しく機能することを確認できます。
    • parseErrorTests 配列にも、StampNano が厳密に9桁の精度を要求することを示すエラーケース(例: .000000 のような6桁の入力)と、余分なテキストがある場合のエラーケースが追加されました。

これらの変更により、time.Parsetime.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言語の time パッケージに関する公式ドキュメント: https://pkg.go.dev/time
  • Go言語の time.Parse 関数の使い方に関する一般的な情報源 (例: Go by Example, A Tour of Goなど)
  • Go言語におけるパニックとエラーハンドリングに関する情報