1# This code was originally contributed by Jeffrey Harris.
2import datetime
3import struct
4
5from six.moves import winreg
6from six import text_type
7
8try:
9    import ctypes
10    from ctypes import wintypes
11except ValueError:
12    # ValueError is raised on non-Windows systems for some horrible reason.
13    raise ImportError("Running tzwin on non-Windows system")
14
15from ._common import tzname_in_python2, _tzinfo
16from ._common import tzrangebase
17
18__all__ = ["tzwin", "tzwinlocal", "tzres"]
19
20ONEWEEK = datetime.timedelta(7)
21
22TZKEYNAMENT = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones"
23TZKEYNAME9X = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Time Zones"
24TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
25
26
27def _settzkeyname():
28    handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
29    try:
30        winreg.OpenKey(handle, TZKEYNAMENT).Close()
31        TZKEYNAME = TZKEYNAMENT
32    except WindowsError:
33        TZKEYNAME = TZKEYNAME9X
34    handle.Close()
35    return TZKEYNAME
36
37TZKEYNAME = _settzkeyname()
38
39
40class tzres(object):
41    """
42    Class for accessing `tzres.dll`, which contains timezone name related
43    resources.
44
45    .. versionadded:: 2.5.0
46    """
47    p_wchar = ctypes.POINTER(wintypes.WCHAR)        # Pointer to a wide char
48
49    def __init__(self, tzres_loc='tzres.dll'):
50        # Load the user32 DLL so we can load strings from tzres
51        user32 = ctypes.WinDLL('user32')
52
53        # Specify the LoadStringW function
54        user32.LoadStringW.argtypes = (wintypes.HINSTANCE,
55                                       wintypes.UINT,
56                                       wintypes.LPWSTR,
57                                       ctypes.c_int)
58
59        self.LoadStringW = user32.LoadStringW
60        self._tzres = ctypes.WinDLL(tzres_loc)
61        self.tzres_loc = tzres_loc
62
63    def load_name(self, offset):
64        """
65        Load a timezone name from a DLL offset (integer).
66
67        >>> from dateutil.tzwin import tzres
68        >>> tzr = tzres()
69        >>> print(tzr.load_name(112))
70        'Eastern Standard Time'
71
72        :param offset:
73            A positive integer value referring to a string from the tzres dll.
74
75        ..note:
76            Offsets found in the registry are generally of the form
77            `@tzres.dll,-114`. The offset in this case if 114, not -114.
78
79        """
80        resource = self.p_wchar()
81        lpBuffer = ctypes.cast(ctypes.byref(resource), wintypes.LPWSTR)
82        nchar = self.LoadStringW(self._tzres._handle, offset, lpBuffer, 0)
83        return resource[:nchar]
84
85    def name_from_string(self, tzname_str):
86        """
87        Parse strings as returned from the Windows registry into the time zone
88        name as defined in the registry.
89
90        >>> from dateutil.tzwin import tzres
91        >>> tzr = tzres()
92        >>> print(tzr.name_from_string('@tzres.dll,-251'))
93        'Dateline Daylight Time'
94        >>> print(tzr.name_from_string('Eastern Standard Time'))
95        'Eastern Standard Time'
96
97        :param tzname_str:
98            A timezone name string as returned from a Windows registry key.
99
100        :return:
101            Returns the localized timezone string from tzres.dll if the string
102            is of the form `@tzres.dll,-offset`, else returns the input string.
103        """
104        if not tzname_str.startswith('@'):
105            return tzname_str
106
107        name_splt = tzname_str.split(',-')
108        try:
109            offset = int(name_splt[1])
110        except:
111            raise ValueError("Malformed timezone string.")
112
113        return self.load_name(offset)
114
115
116class tzwinbase(tzrangebase):
117    """tzinfo class based on win32's timezones available in the registry."""
118    def __init__(self):
119        raise NotImplementedError('tzwinbase is an abstract base class')
120
121    def __eq__(self, other):
122        # Compare on all relevant dimensions, including name.
123        if not isinstance(other, tzwinbase):
124            return NotImplemented
125
126        return  (self._std_offset == other._std_offset and
127                 self._dst_offset == other._dst_offset and
128                 self._stddayofweek == other._stddayofweek and
129                 self._dstdayofweek == other._dstdayofweek and
130                 self._stdweeknumber == other._stdweeknumber and
131                 self._dstweeknumber == other._dstweeknumber and
132                 self._stdhour == other._stdhour and
133                 self._dsthour == other._dsthour and
134                 self._stdminute == other._stdminute and
135                 self._dstminute == other._dstminute and
136                 self._std_abbr == other._std_abbr and
137                 self._dst_abbr == other._dst_abbr)
138
139    @staticmethod
140    def list():
141        """Return a list of all time zones known to the system."""
142        with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
143            with winreg.OpenKey(handle, TZKEYNAME) as tzkey:
144                result = [winreg.EnumKey(tzkey, i)
145                          for i in range(winreg.QueryInfoKey(tzkey)[0])]
146        return result
147
148    def display(self):
149        return self._display
150
151    def transitions(self, year):
152        """
153        For a given year, get the DST on and off transition times, expressed
154        always on the standard time side. For zones with no transitions, this
155        function returns ``None``.
156
157        :param year:
158            The year whose transitions you would like to query.
159
160        :return:
161            Returns a :class:`tuple` of :class:`datetime.datetime` objects,
162            ``(dston, dstoff)`` for zones with an annual DST transition, or
163            ``None`` for fixed offset zones.
164        """
165
166        if not self.hasdst:
167            return None
168
169        dston = picknthweekday(year, self._dstmonth, self._dstdayofweek,
170                               self._dsthour, self._dstminute,
171                               self._dstweeknumber)
172
173        dstoff = picknthweekday(year, self._stdmonth, self._stddayofweek,
174                                self._stdhour, self._stdminute,
175                                self._stdweeknumber)
176
177        # Ambiguous dates default to the STD side
178        dstoff -= self._dst_base_offset
179
180        return dston, dstoff
181
182    def _get_hasdst(self):
183        return self._dstmonth != 0
184
185    @property
186    def _dst_base_offset(self):
187        return self._dst_base_offset_
188
189
190class tzwin(tzwinbase):
191
192    def __init__(self, name):
193        self._name = name
194
195        # multiple contexts only possible in 2.7 and 3.1, we still support 2.6
196        with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
197            tzkeyname = text_type("{kn}\{name}").format(kn=TZKEYNAME, name=name)
198            with winreg.OpenKey(handle, tzkeyname) as tzkey:
199                keydict = valuestodict(tzkey)
200
201        self._std_abbr = keydict["Std"]
202        self._dst_abbr = keydict["Dlt"]
203
204        self._display = keydict["Display"]
205
206        # See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm
207        tup = struct.unpack("=3l16h", keydict["TZI"])
208        stdoffset = -tup[0]-tup[1]          # Bias + StandardBias * -1
209        dstoffset = stdoffset-tup[2]        # + DaylightBias * -1
210        self._std_offset = datetime.timedelta(minutes=stdoffset)
211        self._dst_offset = datetime.timedelta(minutes=dstoffset)
212
213        # for the meaning see the win32 TIME_ZONE_INFORMATION structure docs
214        # http://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx
215        (self._stdmonth,
216         self._stddayofweek,   # Sunday = 0
217         self._stdweeknumber,  # Last = 5
218         self._stdhour,
219         self._stdminute) = tup[4:9]
220
221        (self._dstmonth,
222         self._dstdayofweek,   # Sunday = 0
223         self._dstweeknumber,  # Last = 5
224         self._dsthour,
225         self._dstminute) = tup[12:17]
226
227        self._dst_base_offset_ = self._dst_offset - self._std_offset
228        self.hasdst = self._get_hasdst()
229
230    def __repr__(self):
231        return "tzwin(%s)" % repr(self._name)
232
233    def __reduce__(self):
234        return (self.__class__, (self._name,))
235
236
237class tzwinlocal(tzwinbase):
238    def __init__(self):
239        with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
240            with winreg.OpenKey(handle, TZLOCALKEYNAME) as tzlocalkey:
241                keydict = valuestodict(tzlocalkey)
242
243            self._std_abbr = keydict["StandardName"]
244            self._dst_abbr = keydict["DaylightName"]
245
246            try:
247                tzkeyname = text_type('{kn}\{sn}').format(kn=TZKEYNAME,
248                                                          sn=self._std_abbr)
249                with winreg.OpenKey(handle, tzkeyname) as tzkey:
250                    _keydict = valuestodict(tzkey)
251                    self._display = _keydict["Display"]
252            except OSError:
253                self._display = None
254
255        stdoffset = -keydict["Bias"]-keydict["StandardBias"]
256        dstoffset = stdoffset-keydict["DaylightBias"]
257
258        self._std_offset = datetime.timedelta(minutes=stdoffset)
259        self._dst_offset = datetime.timedelta(minutes=dstoffset)
260
261        # For reasons unclear, in this particular key, the day of week has been
262        # moved to the END of the SYSTEMTIME structure.
263        tup = struct.unpack("=8h", keydict["StandardStart"])
264
265        (self._stdmonth,
266         self._stdweeknumber,  # Last = 5
267         self._stdhour,
268         self._stdminute) = tup[1:5]
269
270        self._stddayofweek = tup[7]
271
272        tup = struct.unpack("=8h", keydict["DaylightStart"])
273
274        (self._dstmonth,
275         self._dstweeknumber,  # Last = 5
276         self._dsthour,
277         self._dstminute) = tup[1:5]
278
279        self._dstdayofweek = tup[7]
280
281        self._dst_base_offset_ = self._dst_offset - self._std_offset
282        self.hasdst = self._get_hasdst()
283
284    def __repr__(self):
285        return "tzwinlocal()"
286
287    def __str__(self):
288        # str will return the standard name, not the daylight name.
289        return "tzwinlocal(%s)" % repr(self._std_abbr)
290
291    def __reduce__(self):
292        return (self.__class__, ())
293
294
295def picknthweekday(year, month, dayofweek, hour, minute, whichweek):
296    """ dayofweek == 0 means Sunday, whichweek 5 means last instance """
297    first = datetime.datetime(year, month, 1, hour, minute)
298
299    # This will work if dayofweek is ISO weekday (1-7) or Microsoft-style (0-6),
300    # Because 7 % 7 = 0
301    weekdayone = first.replace(day=((dayofweek - first.isoweekday()) % 7) + 1)
302    wd = weekdayone + ((whichweek - 1) * ONEWEEK)
303    if (wd.month != month):
304        wd -= ONEWEEK
305
306    return wd
307
308
309def valuestodict(key):
310    """Convert a registry key's values to a dictionary."""
311    dout = {}
312    size = winreg.QueryInfoKey(key)[1]
313    tz_res = None
314
315    for i in range(size):
316        key_name, value, dtype = winreg.EnumValue(key, i)
317        if dtype == winreg.REG_DWORD or dtype == winreg.REG_DWORD_LITTLE_ENDIAN:
318            # If it's a DWORD (32-bit integer), it's stored as unsigned - convert
319            # that to a proper signed integer
320            if value & (1 << 31):
321                value = value - (1 << 32)
322        elif dtype == winreg.REG_SZ:
323            # If it's a reference to the tzres DLL, load the actual string
324            if value.startswith('@tzres'):
325                tz_res = tz_res or tzres()
326                value = tz_res.name_from_string(value)
327
328            value = value.rstrip('\x00')    # Remove trailing nulls
329
330        dout[key_name] = value
331
332    return dout
333