ICU-444 add Win32-specific host timezone detection code

X-SVN-Rev: 11764
This commit is contained in:
Alan Liu 2003-05-02 00:07:35 +00:00
parent d00a183d4e
commit e914485397

View file

@ -656,6 +656,491 @@ static char* u_bottomNBytesOfDouble(double* d, int n)
platform with new implementations.
---------------------------------------------------------------------------*/
/* Win32 time zone detection ------------------------------------------------ */
#ifdef WIN32
/*
This code attempts to detect the Windows time zone, as set in the
Windows Date and Time control panel. It attempts to work on
multiple flavors of Windows (9x, Me, NT, 2000, XP) and on localized
installs. It works by directly interrogating the registry and
comparing the data there with the data returned by the
GetTimeZoneInformation API, along with some other strategies. The
registry contains time zone data under one of two keys (depending on
the flavor of Windows):
HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Time Zones\
HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones\
Under this key are several subkeys, one for each time zone. These
subkeys are named "Pacific" on Win9x/Me and "Pacific Standard Time"
on WinNT/2k/XP. There are some other wrinkles; see the code for
details. The subkey name is NOT LOCALIZED, allowing us to support
localized installs.
Under the subkey are data values. We care about:
Std Standard time display name, localized
TZI Binary block of data
The TZI data is of particular interest. It contains the offset, two
more offsets for standard and daylight time, and the start and end
rules. This is the same data returned by the GetTimeZoneInformation
API. The API may modify the data on the way out, so we have to be
careful, but essentially we do a binary comparison against the TZI
blocks of various registry keys. When we find a match, we know what
time zone Windows is set to. Since the registry key is not
localized, we can then translate the key through a simple table
lookup into the corresponding ICU time zone.
This strategy doesn't always work because there are zones which
share an offset and rules, so more than one TZI block will match.
For example, both Tokyo and Seoul are at GMT+9 with no DST rules;
their TZI blocks are identical. For these cases, we fall back to a
name lookup. We attempt to match the display name as stored in the
registry for the current zone to the display name stored in the
registry for various Windows zones. By comparing the registry data
directly we avoid conversion complications.
Author: Alan Liu
Since: ICU 2.6
Based on original code by Carl Brown <cbrown@xnetinc.com>
*/
static LONG openTZRegKey(HKEY* hkey, const char* tziname, int Wintype);
/**
* Layout of the binary registry data under the "TZI" key.
*/
typedef struct {
LONG Bias;
LONG StandardBias;
LONG DaylightBias;
SYSTEMTIME StandardDate;
SYSTEMTIME DaylightDate;
} TZI;
typedef struct {
const char* icuid;
const char* winid;
} WindowsICUMap;
/**
* Mapping between Windows zone IDs and ICU zone IDs. This list has
* been mechanically checked; all zone offsets match (most important)
* and city names match the display city names (where possible). The
* presence or absence of DST differs in some cases, but this is
* acceptable as long as the zone is semantically the same (which has
* been manually checked).
*
* Windows 9x/Me zone IDs are listed as "Pacific" rather than "Pacific
* Standard Time", which is seen in NT/2k/XP. This is fixed-up at
* runtime as needed. The one exception is "Mexico Standard Time 2",
* which is not present on Windows 9x/Me.
*
* Zones that are not unique under Offset+Rules should be grouped
* together for efficiency (see code below). In addition, rules must
* be grouped so that all zones of a single offset are together. Any
* other organization is not required.
*
* NOTE: Etc/GMT+12 is CORRECT for offset GMT-12:00. Consult
* documentation elsewhere for an explanation.
*/
static const WindowsICUMap ZONE_MAP[] = {
"Etc/GMT+12", "Dateline", /* S (GMT-12:00) International Date Line West */
"Pacific/Apia", "Samoa", /* S (GMT-11:00) Midway Island, Samoa */
"Pacific/Honolulu", "Hawaiian", /* S (GMT-10:00) Hawaii */
"America/Anchorage", "Alaskan", /* D (GMT-09:00) Alaska */
"America/Los_Angeles", "Pacific", /* D (GMT-08:00) Pacific Time (US & Canada); Tijuana */
"America/Phoenix", "US Mountain", /* S (GMT-07:00) Arizona */
"America/Denver", "Mountain", /* D (GMT-07:00) Mountain Time (US & Canada) */
"America/Chihuahua", "Mexico Standard Time 2", /* D (GMT-07:00) Chihuahua, La Paz, Mazatlan */
"America/Managua", "Central America", /* S (GMT-06:00) Central America */
"America/Regina", "Canada Central", /* S (GMT-06:00) Saskatchewan */
"America/Mexico_City", "Mexico", /* D (GMT-06:00) Guadalajara, Mexico City, Monterrey */
"America/Chicago", "Central", /* D (GMT-06:00) Central Time (US & Canada) */
"America/Indianapolis", "US Eastern", /* S (GMT-05:00) Indiana (East) */
"America/Bogota", "SA Pacific", /* S (GMT-05:00) Bogota, Lima, Quito */
"America/New_York", "Eastern", /* D (GMT-05:00) Eastern Time (US & Canada) */
"America/Caracas", "SA Western", /* S (GMT-04:00) Caracas, La Paz */
"America/Santiago", "Pacific SA", /* D (GMT-04:00) Santiago */
"America/Halifax", "Atlantic", /* D (GMT-04:00) Atlantic Time (Canada) */
"America/St_Johns", "Newfoundland", /* D (GMT-03:30) Newfoundland */
"America/Buenos_Aires", "SA Eastern", /* S (GMT-03:00) Buenos Aires, Georgetown */
"America/Godthab", "Greenland", /* D (GMT-03:00) Greenland */
"America/Sao_Paulo", "E. South America", /* D (GMT-03:00) Brasilia */
"America/Noronha", "Mid-Atlantic", /* D (GMT-02:00) Mid-Atlantic */
"Atlantic/Cape_Verde", "Cape Verde", /* S (GMT-01:00) Cape Verde Is. */
"Atlantic/Azores", "Azores", /* D (GMT-01:00) Azores */
"Africa/Casablanca", "Greenwich", /* S (GMT) Casablanca, Monrovia */
"Europe/London", "GMT", /* D (GMT) Greenwich Mean Time : Dublin, Edinburgh, Lisbon, London */
"Africa/Lagos", "W. Central Africa", /* S (GMT+01:00) West Central Africa */
"Europe/Berlin", "W. Europe", /* D (GMT+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna */
"Europe/Paris", "Romance", /* D (GMT+01:00) Brussels, Copenhagen, Madrid, Paris */
"Europe/Sarajevo", "Central European", /* D (GMT+01:00) Sarajevo, Skopje, Warsaw, Zagreb */
"Europe/Belgrade", "Central Europe", /* D (GMT+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague */
"Africa/Johannesburg", "South Africa", /* S (GMT+02:00) Harare, Pretoria */
"Asia/Jerusalem", "Israel", /* S (GMT+02:00) Jerusalem */
"Europe/Istanbul", "GTB", /* D (GMT+02:00) Athens, Istanbul, Minsk */
"Europe/Helsinki", "FLE", /* D (GMT+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius */
"Africa/Cairo", "Egypt", /* D (GMT+02:00) Cairo */
"Europe/Bucharest", "E. Europe", /* D (GMT+02:00) Bucharest */
"Africa/Nairobi", "E. Africa", /* S (GMT+03:00) Nairobi */
"Asia/Riyadh", "Arab", /* S (GMT+03:00) Kuwait, Riyadh */
"Europe/Moscow", "Russian", /* D (GMT+03:00) Moscow, St. Petersburg, Volgograd */
"Asia/Baghdad", "Arabic", /* D (GMT+03:00) Baghdad */
"Asia/Tehran", "Iran", /* D (GMT+03:30) Tehran */
"Asia/Muscat", "Arabian", /* S (GMT+04:00) Abu Dhabi, Muscat */
"Asia/Tbilisi", "Caucasus", /* D (GMT+04:00) Baku, Tbilisi, Yerevan */
"Asia/Kabul", "Afghanistan", /* S (GMT+04:30) Kabul */
"Asia/Karachi", "West Asia", /* S (GMT+05:00) Islamabad, Karachi, Tashkent */
"Asia/Yekaterinburg", "Ekaterinburg", /* D (GMT+05:00) Ekaterinburg */
"Asia/Calcutta", "India", /* S (GMT+05:30) Chennai, Kolkata, Mumbai, New Delhi */
"Asia/Katmandu", "Nepal", /* S (GMT+05:45) Kathmandu */
"Asia/Colombo", "Sri Lanka", /* S (GMT+06:00) Sri Jayawardenepura */
"Asia/Dhaka", "Central Asia", /* S (GMT+06:00) Astana, Dhaka */
"Asia/Novosibirsk", "N. Central Asia", /* D (GMT+06:00) Almaty, Novosibirsk */
"Asia/Rangoon", "Myanmar", /* S (GMT+06:30) Rangoon */
"Asia/Bangkok", "SE Asia", /* S (GMT+07:00) Bangkok, Hanoi, Jakarta */
"Asia/Krasnoyarsk", "North Asia", /* D (GMT+07:00) Krasnoyarsk */
"Australia/Perth", "W. Australia", /* S (GMT+08:00) Perth */
"Asia/Taipei", "Taipei", /* S (GMT+08:00) Taipei */
"Asia/Singapore", "Singapore", /* S (GMT+08:00) Kuala Lumpur, Singapore */
"Asia/Hong_Kong", "China", /* S (GMT+08:00) Beijing, Chongqing, Hong Kong, Urumqi */
"Asia/Irkutsk", "North Asia East", /* D (GMT+08:00) Irkutsk, Ulaan Bataar */
"Asia/Tokyo", "Tokyo", /* S (GMT+09:00) Osaka, Sapporo, Tokyo */
"Asia/Seoul", "Korea", /* S (GMT+09:00) Seoul */
"Asia/Yakutsk", "Yakutsk", /* D (GMT+09:00) Yakutsk */
"Australia/Darwin", "AUS Central", /* S (GMT+09:30) Darwin */
"Australia/Adelaide", "Cen. Australia", /* D (GMT+09:30) Adelaide */
"Pacific/Guam", "West Pacific", /* S (GMT+10:00) Guam, Port Moresby */
"Australia/Brisbane", "E. Australia", /* S (GMT+10:00) Brisbane */
"Asia/Vladivostok", "Vladivostok", /* D (GMT+10:00) Vladivostok */
"Australia/Hobart", "Tasmania", /* D (GMT+10:00) Hobart */
"Australia/Sydney", "AUS Eastern", /* D (GMT+10:00) Canberra, Melbourne, Sydney */
"Asia/Magadan", "Central Pacific", /* S (GMT+11:00) Magadan, Solomon Is., New Caledonia */
"Pacific/Fiji", "Fiji", /* S (GMT+12:00) Fiji, Kamchatka, Marshall Is. */
"Pacific/Auckland", "New Zealand", /* D (GMT+12:00) Auckland, Wellington */
"Pacific/Tongatapu", "Tonga", /* S (GMT+13:00) Nuku'alofa */
NULL, NULL
};
typedef struct {
const char* winid;
const char* altwinid;
} WindowsZoneRemap;
/**
* If a lookup fails, we attempt to remap certain Windows ids to
* alternate Windows ids. If the alternate listed here begins with
* '-', we use it as is (without the '-'). If it begins with '+', we
* append a " Standard Time" if appropriate.
*/
WindowsZoneRemap ZONE_REMAP[] = {
"Central European", "-Warsaw",
"Central Europe", "-Prague Bratislava",
"China", "-Beijing",
"Greenwich", "+GMT",
"GTB", "+GFT",
"Arab", "+Saudi Arabia",
"SE Asia", "+Bangkok",
"AUS Eastern", "+Sydney",
NULL, NULL,
};
/**
* Various registry keys and key fragments.
*/
static const char* CURRENT_ZONE_REGKEY = "SYSTEM\\CurrentControlSet\\Control\\TimeZoneInformation\\";
static const char* STANDARD_NAME_REGKEY = "StandardName";
static const char* STANDARD_TIME_REGKEY = " Standard Time";
static const char* TZI_REGKEY = "TZI";
static const char* STD_REGKEY = "Std";
/**
* HKLM subkeys used to probe for the flavor of Windows. Note that we
* specifically check for the "GMT" zone sub key; this is present on
* NT, but on XP has become "GMT Standard Time". We need to
* discriminate between these cases.
*/
static const char* WIN_TYPE_PROBE_REGKEY[] = {
/* WIN_9X_ME_TYPE */
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Time Zones",
/* WIN_NT_TYPE */
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones\\GMT"
/* otherwise: WIN_2K_XP_TYPE */
};
/**
* The time zone root subkeys (under HKLM) for different flavors of
* Windows.
*/
static const char* TZ_REGKEY[] = {
/* WIN_9X_ME_TYPE */
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Time Zones\\",
/* WIN_NT_TYPE | WIN_2K_XP_TYPE */
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones\\"
};
/**
* Flavor of Windows, from our perspective. Not a real OS version,
* but rather the flavor of the layout of the time zone information in
* the registry.
*/
enum {
WIN_9X_ME_TYPE = 0,
WIN_NT_TYPE = 1,
WIN_2K_XP_TYPE = 2
};
/**
* Main Windows time zone detection function. Returns the Windows
* time zone, translated to an ICU time zone, or NULL upon failure.
*/
static const char* detectWindowsTimeZone() {
int winType;
LONG result;
HKEY hkey;
TZI tziKey;
TZI tziReg;
DWORD cbData = sizeof(TZI);
TIME_ZONE_INFORMATION apiTZI;
char stdName[32];
DWORD stdNameSize;
char stdRegName[64];
DWORD stdRegNameSize;
int firstMatch, lastMatch;
int j;
/* Detect the version of windows by trying to open a sequence of
probe keys. We don't use the OS version API because what we
really want to know is how the registry is laid out.
Specifically, is it 9x/Me or not, and is it "GMT" or "GMT
Standard Time". */
for (winType=0; winType<2; ++winType) {
result = RegOpenKeyEx(HKEY_LOCAL_MACHINE,
WIN_TYPE_PROBE_REGKEY[winType],
0,
KEY_QUERY_VALUE,
&hkey);
RegCloseKey(hkey);
if (result == ERROR_SUCCESS) {
break;
}
}
/* Obtain TIME_ZONE_INFORMATION from the API, and then convert it
to TZI. We could also interrogate the registry directly; we do
this below if needed. */
memset(&apiTZI, 0, sizeof(apiTZI));
GetTimeZoneInformation(&apiTZI);
tziKey.Bias = apiTZI.Bias;
memcpy((char *)&tziKey.StandardDate, (char*)&apiTZI.StandardDate,
sizeof(apiTZI.StandardDate));
memcpy((char *)&tziKey.DaylightDate, (char*)&apiTZI.DaylightDate,
sizeof(apiTZI.DaylightDate));
/* For each zone that can be identified by Offset+Rules, see if we
have a match. Continue scanning after finding a match,
recording the index of the first and the last match. We have
to do this because some zones are not unique under
Offset+Rules. */
firstMatch = lastMatch = -1;
for (j=0; ZONE_MAP[j].icuid; j++) {
result = openTZRegKey(&hkey, ZONE_MAP[j].winid, winType);
if (result == ERROR_SUCCESS) {
result = RegQueryValueEx(hkey,
TZI_REGKEY,
NULL,
NULL,
(LPBYTE)&tziReg,
&cbData);
}
RegCloseKey(hkey);
if (result == ERROR_SUCCESS) {
/* Windows alters the DaylightBias in some situations.
Using the bias and the rules suffices, so overwrite
these unreliable fields. */
tziKey.StandardBias = tziReg.StandardBias;
tziKey.DaylightBias = tziReg.DaylightBias;
if (memcmp((char *)&tziKey, (char*)&tziReg,
sizeof(tziKey)) == 0) {
if (firstMatch < 0) {
firstMatch = j;
}
lastMatch = j;
}
/* Assume that offsets are grouped together, and bail out
when we've scanned everything with a matching
offset. */
if (firstMatch >= 0 && tziKey.Bias != tziReg.Bias) {
break;
}
}
}
/* This should never happen; if it does it means our table doesn't
match Windows AT ALL, perhaps because this is post-XP? */
if (firstMatch < 0) {
return NULL;
}
if (firstMatch == lastMatch) { /* Offset+Rules matched exactly once */
return ZONE_MAP[firstMatch].icuid;
}
/* Offset+Rules lookup yielded >= 2 matches. Try to match the
localized display name. Get the name from the registry (not
the API). This avoids conversion issues. Use the standard
name, since Windows modifies the daylight name to match the
standard name if there is no DST. */
result = RegOpenKeyEx(HKEY_LOCAL_MACHINE,
CURRENT_ZONE_REGKEY,
0,
KEY_QUERY_VALUE,
&hkey);
if (result != ERROR_SUCCESS) {
RegCloseKey(hkey); /* should not happen */
return NULL;
}
stdNameSize = sizeof(stdName);
result = RegQueryValueEx(hkey,
(LPTSTR)STANDARD_NAME_REGKEY,
NULL,
NULL,
(LPBYTE)stdName,
&stdNameSize);
RegCloseKey(hkey);
/* Scan through our list again (just the range of zones that
matched) and try for a name match. */
for (j=firstMatch; j<=lastMatch; j++) {
result = openTZRegKey(&hkey, ZONE_MAP[j].winid, winType);
if (result == ERROR_SUCCESS) {
stdRegNameSize = sizeof(stdRegName);
result = RegQueryValueEx(hkey,
(LPTSTR)STD_REGKEY,
NULL,
NULL,
(LPBYTE)stdRegName,
&stdRegNameSize);
}
RegCloseKey(hkey);
if (result == ERROR_SUCCESS) {
if (memcmp(stdName, stdRegName, stdNameSize) == 0) {
break;
}
}
}
/* One of the display names should match; if not, take the first
Offset+Rules match. */
return ZONE_MAP[j <= lastMatch ? j : firstMatch].icuid;
}
/**
* Auxiliary Windows time zone function. Attempts to open the given
* Windows time zone ID as a registry key. Returns ERROR_SUCCESS if
* successful. Caller must close the registry key. Handles
* variations in the resource layout in different flavors of Windows.
*
* @param hkey output parameter to receive opened registry key
* @param winid Windows zone ID, e.g., "Pacific", without the
* " Standard Time" suffix (if any). Special case "Mexico Standard Time 2"
* allowed.
* @param winType Windows flavor (WIN_9X_ME_TYPE, etc.)
* @return ERROR_SUCCESS upon success
*/
static LONG openTZRegKey(HKEY *hkey, const char* winid, int winType) {
LONG result;
char subKeyName[96];
char* name;
int i;
strcpy(subKeyName, TZ_REGKEY[(winType == WIN_9X_ME_TYPE) ? 0 : 1]);
name = &subKeyName[strlen(subKeyName)];
strcat(subKeyName, winid);
if (winType != WIN_9X_ME_TYPE) {
/* Don't modify "Mexico Standard Time 2", which does not occur
on WIN_9X_ME_TYPE. Also, if the type is WIN_NT_TYPE, then
in practice this means the GMT key is not followed by
" Standard Time", so don't append in that case. */
int isMexico2 = (winid[strlen(winid)- 1] == '2');
if (!isMexico2 &&
!(winType == WIN_NT_TYPE && strcmp(winid, "GMT") == 0)) {
strcat(subKeyName, STANDARD_TIME_REGKEY);
}
}
result = RegOpenKeyEx(HKEY_LOCAL_MACHINE,
subKeyName,
0,
KEY_QUERY_VALUE,
hkey);
if (result != ERROR_SUCCESS) {
/* If the primary lookup fails, try to remap the Windows zone
ID, according to the remapping table. */
for (i=0; ZONE_REMAP[i].winid; ++i) {
if (strcmp(winid, ZONE_REMAP[i].winid) == 0) {
strcpy(name, ZONE_REMAP[i].altwinid + 1);
if (*(ZONE_REMAP[i].altwinid) == '+' &&
winType != WIN_9X_ME_TYPE) {
strcat(subKeyName, STANDARD_TIME_REGKEY);
}
result = RegOpenKeyEx(HKEY_LOCAL_MACHINE,
subKeyName,
0,
KEY_QUERY_VALUE,
hkey);
break;
}
}
}
return result;
}
#endif /*WIN32*/
/* Generic time zone layer -------------------------------------------------- */
/* Time zone utilities */
U_CAPI void U_EXPORT2
uprv_tzset()
@ -705,6 +1190,13 @@ extern U_IMPORT char *_tzname[2];
U_CAPI char* U_EXPORT2
uprv_tzname(int n)
{
#ifdef WIN32
char* id = (char*) detectWindowsTimeZone();
if (id != NULL) {
return id;
}
#endif
#ifdef U_TZNAME
return U_TZNAME[n];
#else