[インデックス 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レジストリからタイムゾーンの英語名を取得する新しいロジックにあります。
-
getKeyValue(kh syscall.Handle, kname string) (string, error)
:- この関数は、開かれたレジストリキー
kh
の下にある指定された名前kname
の文字列値を取得します。 syscall.RegQueryValueEx
を使用してレジストリから値を読み取ります。buf
というuint16
の配列をバッファとして使用し、unsafe.Pointer
を介してバイト配列としてAPIに渡します。- 取得した値の型が
syscall.REG_SZ
(null終端文字列) であることを確認し、syscall.UTF16ToString
でGoの文字列に変換して返します。
- この関数は、開かれたレジストリキー
-
matchZoneKey(zones syscall.Handle, kname string, stdname, dstname string) (matched bool, err2 error)
:- この関数は、
zones
というレジストリキーの下にあるkname
というサブキーを開き、そのサブキー内のStd
(標準名) とDlt
(夏時間名) の値が、引数で与えられたstdname
とdstname
にそれぞれ一致するかどうかをチェックします。 syscall.RegOpenKeyEx
でサブキーを開き、getKeyValue
を使ってStd
とDlt
の値を取得します。- 一致した場合
true
を、一致しない場合false
を返します。エラーが発生した場合はエラーを返します。
- この関数は、
-
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
を呼び出し、そのサブキー内のStd
とDlt
の値が、検索対象のstdname
とdstname
に一致するかどうかを確認します。 - 一致するサブキーが見つかった場合、そのサブキーの名前(英語名)を返します。
- すべてのサブキーを調べても一致するものが見つからない場合、エラーを返します。
- この関数は、引数で与えられたローカライズされた標準名
-
abbrev(z *syscall.Timezoneinformation) (std, dst string)
の変更:abbrev
関数は、Windowsシステムから取得したタイムゾーン情報z
から、標準時と夏時間の略称を決定する役割を担っています。- 変更前は、
syscall.UTF16ToString
で変換したstdName
を直接abbrs
マップで検索していました。見つからない場合は、extractCAPS
(大文字を抽出する関数)を使って略称を生成していました。 - 変更後は、
stdName
がabbrs
マップに見つからなかった場合、toEnglishName
関数を呼び出して、ローカライズされたstdName
とdstName
に対応する英語名をレジストリから検索するようになりました。 - 英語名が正常に取得できた場合、その英語名を使って再度
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]))
の部分は、buf
がuint16
の配列であるのに対し、RegQueryValueEx
がバイトポインタを期待するため、型変換を行っています。unsafe.Pointer
を使用することで、Goの型システムを一時的にバイパスし、uint16
の配列の先頭アドレスをバイトポインタとしてAPIに渡しています。n := uint32(len(buf) * 2)
は、RegQueryValueEx
がバイト数を期待するため、uint16
の配列の長さをバイト数に変換しています(uint16
は2バイト)。if typ != syscall.REG_SZ
は、取得した値が文字列型(REG_SZ
)であることを確認しています。これは、タイムゾーン名が文字列として格納されていることを前提としているためです。
-
matchZoneKey
:- この関数は、特定のタイムゾーンのサブキー(
kname
)を開き、その中のStd
とDlt
の値が、引数で与えられた標準名と夏時間名に一致するかどうかを検証します。 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
のブロックが、stdName
がabbrs
マップに見つからなかった場合の処理です。- このブロック内で、
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
パッケージのドキュメント