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

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

このコミットは、Go言語の time パッケージにおけるWindows環境でのタイムゾーン情報の取り扱いに関するバグ修正です。具体的には、非英語Windowsシステムにおいて、タイムゾーンの略称(例: JST, EST)を正しく特定できない問題を解決します。

コミット

commit 231dfd9049a1344fc98ee3cd950473b2f986c28f
Author: Alex Brainman <alex.brainman@gmail.com>
Date:   Wed Jul 10 15:34:24 2013 +1000

    time: find correct zone abbreviations even on non-English windows systems
    
    Fixes #5783
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/10956043

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

https://github.com/golang/go/commit/231dfd9049a1344fc98ee3cd950473b2f986c28f

元コミット内容

このコミットは、src/pkg/time/zoneinfo_windows.go ファイルに対して82行の追加と1行の削除を行っています。主な変更点は、Windowsレジストリからタイムゾーン情報を取得する際に、非英語のタイムゾーン名に対応するためのロジックを追加したことです。具体的には、getKeyValue, matchZoneKey, toEnglishName といった新しいヘルパー関数が導入され、abbrev 関数内でこれらの関数を使用して、システムが返すタイムゾーン名が英語でない場合に、対応する英語名をレジストリから検索するようになりました。

変更の背景

Go言語の time パッケージは、システムからタイムゾーン情報を取得して、時刻の表示や計算に利用します。Windows環境では、この情報はレジストリに格納されています。しかし、非英語のWindowsシステムでは、タイムゾーンの標準名(Standard Name)や夏時間名(Daylight Name)がそのシステムのローカル言語で表現されることがあります。

Goの time パッケージ内部では、既知のタイムゾーン略称(例: "EST", "PST", "JST" など)と、それに対応する標準名・夏時間名のマッピングを abbrs というマップで管理しています。このマップは通常、英語のタイムゾーン名に基づいて構築されています。

問題は、非英語のWindowsシステムが返すタイムゾーン名がこの abbrs マップに存在しない場合、Goの time パッケージが正しいタイムゾーン略称を特定できず、代わりにタイムゾーン名の大文字部分を抽出して略称として使用してしまうことでした(例: "日本標準時" から "JST" ではなく "J" を抽出してしまうなど)。これは、特にタイムゾーンの表示において不正確な結果をもたらし、ユーザーエクスペリエンスを損なう可能性がありました。

このコミットは、この問題を解決し、非英語Windowsシステムでも正確なタイムゾーン略称が取得できるようにすることを目的としています。コミットメッセージにある Fixes #5783 は、この問題がGoのIssueトラッカーで報告されていたことを示唆しています。

前提知識の解説

タイムゾーンと略称

タイムゾーンは、地球上の特定の地域で共通して使用される標準時を定義するものです。夏時間(Daylight Saving Time, DST)が導入されている地域では、特定の期間に時間が1時間進められます。タイムゾーンには、通常、標準時と夏時間の両方に対応する略称が存在します(例: 日本標準時 (JST), 東部標準時 (EST), 東部夏時間 (EDT))。

Windowsレジストリとタイムゾーン情報

Windowsオペレーティングシステムは、システム設定やアプリケーション設定をレジストリという階層型データベースに格納しています。タイムゾーンに関する情報もレジストリに保存されており、特に以下のパスに重要な情報が含まれています。

  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones

このキーの下には、各タイムゾーンに対応するサブキーが存在し、それぞれのサブキーにはそのタイムゾーンの標準名(Std)、夏時間名(Dlt)、表示名(Display)、略称(TZIバイナリデータの一部)などの情報が格納されています。これらの名前は、システムの表示言語によってローカライズされている場合があります。

Go言語の syscall パッケージ

Go言語の syscall パッケージは、オペレーティングシステムが提供する低レベルのシステムコールやAPIにアクセスするための機能を提供します。Windows環境では、このパッケージを通じてWin32 APIを呼び出し、レジストリの読み取りやシステム情報の取得などを行うことができます。

  • syscall.RegOpenKeyEx: レジストリキーを開く
  • syscall.RegQueryValueEx: レジストリキーの値を読み取る
  • syscall.RegEnumKeyEx: レジストリキーのサブキーを列挙する
  • syscall.RegCloseKey: 開いたレジストリキーを閉じる
  • syscall.UTF16PtrFromString: Goの文字列をUTF-16エンコードされたポインタに変換する(Win32 APIは通常UTF-16を使用するため)
  • syscall.UTF16ToString: UTF-16エンコードされたバイト列をGoの文字列に変換する

unsafe パッケージ

Go言語の unsafe パッケージは、型安全性をバイパスする操作(ポインタ演算など)を可能にします。これは非常に強力ですが、誤用するとプログラムのクラッシュや未定義の動作を引き起こす可能性があるため、慎重に使用する必要があります。このコミットでは、syscall.RegQueryValueEx の引数としてバイト配列のポインタを渡すために unsafe.Pointer が使用されています。これは、Win32 APIが通常バイト配列を期待するのに対し、Goの配列は型付けされているため、型変換が必要となるためです。

技術的詳細

このコミットの核心は、Windowsレジストリからタイムゾーンの英語名を取得する新しいロジックにあります。

  1. getKeyValue(kh syscall.Handle, kname string) (string, error):

    • この関数は、開かれたレジストリキー kh の下にある指定された名前 kname の文字列値を取得します。
    • syscall.RegQueryValueEx を使用してレジストリから値を読み取ります。
    • buf という uint16 の配列をバッファとして使用し、unsafe.Pointer を介してバイト配列としてAPIに渡します。
    • 取得した値の型が syscall.REG_SZ (null終端文字列) であることを確認し、syscall.UTF16ToString でGoの文字列に変換して返します。
  2. matchZoneKey(zones syscall.Handle, kname string, stdname, dstname string) (matched bool, err2 error):

    • この関数は、zones というレジストリキーの下にある kname というサブキーを開き、そのサブキー内の Std (標準名) と Dlt (夏時間名) の値が、引数で与えられた stdnamedstname にそれぞれ一致するかどうかをチェックします。
    • syscall.RegOpenKeyEx でサブキーを開き、getKeyValue を使って StdDlt の値を取得します。
    • 一致した場合 true を、一致しない場合 false を返します。エラーが発生した場合はエラーを返します。
  3. toEnglishName(stdname, dstname string) (string, error):

    • この関数は、引数で与えられたローカライズされた標準名 stdname と夏時間名 dstname に対応するタイムゾーンの英語名をWindowsレジストリから検索します。
    • まず、HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones キーを開きます。
    • syscall.RegQueryInfoKey を使用して、このキーの下にあるサブキー(個々のタイムゾーンエントリ)の数を取得します。
    • syscall.RegEnumKeyEx をループで呼び出し、各サブキーの名前を列挙します。これらのサブキーの名前がタイムゾーンの英語名に相当します(例: "Tokyo Standard Time", "Eastern Standard Time")。
    • 列挙された各サブキーに対して matchZoneKey を呼び出し、そのサブキー内の StdDlt の値が、検索対象の stdnamedstname に一致するかどうかを確認します。
    • 一致するサブキーが見つかった場合、そのサブキーの名前(英語名)を返します。
    • すべてのサブキーを調べても一致するものが見つからない場合、エラーを返します。
  4. abbrev(z *syscall.Timezoneinformation) (std, dst string) の変更:

    • abbrev 関数は、Windowsシステムから取得したタイムゾーン情報 z から、標準時と夏時間の略称を決定する役割を担っています。
    • 変更前は、syscall.UTF16ToString で変換した stdName を直接 abbrs マップで検索していました。見つからない場合は、extractCAPS(大文字を抽出する関数)を使って略称を生成していました。
    • 変更後は、stdNameabbrs マップに見つからなかった場合、toEnglishName 関数を呼び出して、ローカライズされた stdNamedstName に対応する英語名をレジストリから検索するようになりました。
    • 英語名が正常に取得できた場合、その英語名を使って再度 abbrs マップを検索します。これにより、非英語システムでも正しい略称が取得できるようになります。
    • それでも見つからない場合のみ、以前と同様に extractCAPS を使って略称を生成するフォールバックロジックが適用されます。

この一連の変更により、Goの time パッケージは、Windowsのレジストリに格納されているタイムゾーンのローカライズされた名前を適切に処理し、対応する英語名を見つけることで、正確なタイムゾーン略称を導き出すことが可能になりました。

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

src/pkg/time/zoneinfo_windows.go

--- a/src/pkg/time/zoneinfo_windows.go
+++ b/src/pkg/time/zoneinfo_windows.go
@@ -8,6 +8,78 @@ import (
 	"errors"
 	"runtime"
 	"syscall"
+	"unsafe"
 )
 
 // TODO(rsc): Fall back to copy of zoneinfo files.
@@ -17,6 +18,78 @@ import (
 // The implementation assumes that this year's rules for daylight savings
 // time apply to all previous and future years as well.
 
+// getKeyValue retrieves the string value kname associated with the open registry key kh.
+func getKeyValue(kh syscall.Handle, kname string) (string, error) {
+	var buf [50]uint16 // buf needs to be large enough to fit zone descriptions
+	var typ uint32
+	n := uint32(len(buf) * 2) // RegQueryValueEx's signature expects array of bytes, not uint16
+	p, _ := syscall.UTF16PtrFromString(kname)
+	if err := syscall.RegQueryValueEx(kh, p, nil, &typ, (*byte)(unsafe.Pointer(&buf[0])), &n); err != nil {
+		return "", err
+	}
+	if typ != syscall.REG_SZ { // null terminated strings only
+		return "", errors.New("Key is not string")
+	}
+	return syscall.UTF16ToString(buf[:]), nil
+}
+
+// matchZoneKey checks if stdname and dstname match the corresponding "Std"
+// and "Dlt" key values in the kname key stored under the open registry key zones.
+func matchZoneKey(zones syscall.Handle, kname string, stdname, dstname string) (matched bool, err2 error) {
+	var h syscall.Handle
+	p, _ := syscall.UTF16PtrFromString(kname)
+	if err := syscall.RegOpenKeyEx(zones, p, 0, syscall.KEY_READ, &h); err != nil {
+		return false, err
+	}
+	defer syscall.RegCloseKey(h)
+
+	s, err := getKeyValue(h, "Std")
+	if err != nil {
+		return false, err
+	}
+	if s != stdname {
+		return false, nil
+	}
+	s, err = getKeyValue(h, "Dlt")
+	if err != nil {
+		return false, err
+	}
+	if s != dstname {
+		return false, nil
+	}
+	return true, nil
+}
+
+// toEnglishName searches the registry for an English name of a time zone
+// whose zone names are stdname and dstname and returns the English name.
+func toEnglishName(stdname, dstname string) (string, error) {
+	var zones syscall.Handle
+	p, _ := syscall.UTF16PtrFromString(`SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones`)
+	if err := syscall.RegOpenKeyEx(syscall.HKEY_LOCAL_MACHINE, p, 0, syscall.KEY_READ, &zones); err != nil {
+		return "", err
+	}
+	defer syscall.RegCloseKey(zones)
+
+	var count uint32
+	if err := syscall.RegQueryInfoKey(zones, nil, nil, nil, &count, nil, nil, nil, nil, nil, nil, nil); err != nil {
+		return "", err
+	}
+
+	var buf [50]uint16 // buf needs to be large enough to fit zone descriptions
+	for i := uint32(0); i < count; i++ {
+		n := uint32(len(buf))
+		if syscall.RegEnumKeyEx(zones, i, &buf[0], &n, nil, nil, nil, nil) != nil {
+			continue
+		}
+		kname := syscall.UTF16ToString(buf[:])
+		matched, err := matchZoneKey(zones, kname, stdname, dstname)
+		if err == nil && matched {
+			return kname, nil
+		}
+	}
+	return "", errors.New(`English name for time zone "` + stdname + `" not found in registry`)
+}
+
 // extractCAPS exracts capital letters from description desc.
 func extractCAPS(desc string) string {
 	var short []rune
@@ -33,8 +106,16 @@ func abbrev(z *syscall.Timezoneinformation) (std, dst string) {
 	stdName := syscall.UTF16ToString(z.StandardName[:])
 	a, ok := abbrs[stdName]
 	if !ok {
-		// fallback to using capital letters
 		dstName := syscall.UTF16ToString(z.DaylightName[:])
+		// Perhaps stdName is not English. Try to convert it.
+		englishName, err := toEnglishName(stdName, dstName)
+		if err == nil {
+			a, ok = abbrs[englishName]
+			if ok {
+				return a.std, a.dst
+			}
+		}
+		// fallback to using capital letters
 		return extractCAPS(stdName), extractCAPS(dstName)
 	}
 	return a.std, a.dst

コアとなるコードの解説

新規追加された関数

  • getKeyValue:

    • この関数は、Windowsレジストリから特定のキーの文字列値を取得するための汎用ヘルパーです。
    • syscall.RegQueryValueEx は、レジストリからデータを読み取るためのWin32 API関数です。
    • (*byte)(unsafe.Pointer(&buf[0])) の部分は、bufuint16 の配列であるのに対し、RegQueryValueEx がバイトポインタを期待するため、型変換を行っています。unsafe.Pointer を使用することで、Goの型システムを一時的にバイパスし、uint16 の配列の先頭アドレスをバイトポインタとしてAPIに渡しています。
    • n := uint32(len(buf) * 2) は、RegQueryValueEx がバイト数を期待するため、uint16 の配列の長さをバイト数に変換しています(uint16 は2バイト)。
    • if typ != syscall.REG_SZ は、取得した値が文字列型(REG_SZ)であることを確認しています。これは、タイムゾーン名が文字列として格納されていることを前提としているためです。
  • matchZoneKey:

    • この関数は、特定のタイムゾーンのサブキー(kname)を開き、その中の StdDlt の値が、引数で与えられた標準名と夏時間名に一致するかどうかを検証します。
    • defer syscall.RegCloseKey(h) は、関数が終了する際に開いたレジストリキーハンドル h を確実に閉じるためのものです。これにより、リソースリークを防ぎます。
  • toEnglishName:

    • この関数は、ローカライズされたタイムゾーン名(stdname, dstname)から、対応する英語のタイムゾーン名をレジストリから探し出します。
    • syscall.HKEY_LOCAL_MACHINE は、Windowsレジストリのルートキーの一つで、システム全体の設定が格納されています。
    • SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones は、Windowsが管理するタイムゾーン情報が格納されているレジストリパスです。
    • syscall.RegEnumKeyEx は、指定されたレジストリキーのサブキーを一つずつ列挙するためのWin32 API関数です。この関数をループで呼び出すことで、すべてのタイムゾーンエントリを走査します。
    • 列挙されたサブキーの名前(kname)は、通常、そのタイムゾーンの英語名に対応しています(例: "Tokyo Standard Time")。
    • matchZoneKey を呼び出すことで、列挙された英語名のタイムゾーンが、現在処理しているローカライズされたタイムゾーンと一致するかどうかを確認しています。

abbrev 関数の変更点

  • abbrev 関数は、syscall.Timezoneinformation 構造体からタイムゾーンの略称を抽出するGoの内部関数です。
  • 変更前は、stdName(Windowsから取得した標準名)を直接 abbrs マップで検索していました。abbrs はGoの time パッケージが内部的に持つ、英語のタイムゾーン名と略称のマッピングです。
  • if !ok のブロックが、stdNameabbrs マップに見つからなかった場合の処理です。
  • このブロック内で、toEnglishName(stdName, dstName) が新しく呼び出されています。これは、取得した stdName が英語でない可能性があるため、レジストリから対応する英語名を検索しようと試みます。
  • if err == nil は、英語名の検索が成功した場合の処理です。
  • a, ok = abbrs[englishName] で、取得した英語名を使って再度 abbrs マップを検索します。これにより、非英語システムでも正しい略称(例: "JST")が取得できる可能性が高まります。
  • もし英語名が見つからない、または英語名を使っても abbrs マップに見つからない場合は、最終的なフォールバックとして extractCAPS(タイムゾーン名の大文字を抽出する)が使用されます。

この変更により、Goの time パッケージは、Windowsの多言語環境において、より堅牢にタイムゾーンの略称を特定できるようになりました。

関連リンク

  • Go言語の time パッケージに関する公式ドキュメント: https://pkg.go.dev/time
  • Windowsレジストリのタイムゾーン情報に関するMicrosoftのドキュメント(一般的な情報源)

参考にした情報源リンク

  • Go言語のソースコード(src/pkg/time/zoneinfo_windows.go
  • Windows APIドキュメント(RegOpenKeyEx, RegQueryValueEx, RegEnumKeyEx など)
  • Go言語の syscall パッケージのドキュメント
  • Go言語の unsafe パッケージのドキュメント