1 /********************************************************************\
2  * gnc-timezone.cpp - Retrieve timezone information from OS.        *
3  * Copyright 2014 John Ralls <jralls@ceridwen.us>                   *
4  * Based on work done with Arnel Borja for GLib's gtimezone in 2012.*
5  * This program is free software; you can redistribute it and/or    *
6  * modify it under the terms of the GNU General Public License as   *
7  * published by the Free Software Foundation; either version 2 of   *
8  * the License, or (at your option) any later version.              *
9  *                                                                  *
10  * This program is distributed in the hope that it will be useful,  *
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of   *
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the    *
13  * GNU General Public License for more details.                     *
14  *                                                                  *
15  * You should have received a copy of the GNU General Public License*
16  * along with this program; if not, contact:                        *
17  *                                                                  *
18  * Free Software Foundation           Voice:  +1-617-542-5942       *
19  * 51 Franklin Street, Fifth Floor    Fax:    +1-617-542-2652       *
20  * Boston, MA  02110-1301,  USA       gnu@gnu.org                   *
21 \********************************************************************/
22 
23 #include "gnc-timezone.hpp"
24 
25 #include <string>
26 #include <cstdint>
27 #include <iostream>
28 #include <algorithm>
29 #include <boost/date_time/gregorian/gregorian.hpp>
30 #if PLATFORM(WINDOWS)
31 //We'd prefer to use std::codecvt, but it's not supported by gcc until 5.0.
32 #include <boost/locale/encoding_utf.hpp>
33 #endif
34 extern "C"
35 {
36 #include "qoflog.h"
37 static const QofLogModule log_module = "gnc-timezone";
38 }
39 
40 using namespace gnc::date;
41 
42 using duration = boost::posix_time::time_duration;
43 using time_zone = boost::local_time::custom_time_zone;
44 using dst_offsets = boost::local_time::dst_adjustment_offsets;
45 using calc_rule_ptr = boost::local_time::dst_calc_rule_ptr;
46 using PTZ = boost::local_time::posix_time_zone;
47 
48 const unsigned int TimeZoneProvider::min_year = 1400;
49 const unsigned int TimeZoneProvider::max_year = 9999;
50 
51 template<typename T>
52 T*
endian_swap(T * t)53 endian_swap(T* t)
54 {
55 #if ! WORDS_BIGENDIAN
56     auto memp = reinterpret_cast<unsigned char*>(t);
57     std::reverse(memp, memp + sizeof(T));
58 #endif
59     return t;
60 }
61 
62 #if PLATFORM(WINDOWS)
63 /* libstdc++ to_string is broken on MinGW with no real interest in fixing it.
64  * See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=52015
65  */
66 #if ! COMPILER(MINGW)
67 using std::to_string;
68 #else
69 template<typename T> inline std::string to_string(T num);
70 
71 template<>
72 inline std::string
to_string(unsigned int num)73 to_string<unsigned int>(unsigned int num)
74 {
75     constexpr unsigned int numchars = sizeof num * 3 + 1;
76     char buf [numchars] {};
77     snprintf (buf, numchars, "%u", num);
78     return std::string(buf);
79 }
80 
81 template<>
82 inline std::string
to_string(int num)83 to_string<int>(int num)
84 {
85     constexpr unsigned int numchars = sizeof num * 3 + 1;
86     char buf [numchars];
87     snprintf (buf, numchars, "%d", num);
88     return std::string(buf);
89 }
90 #endif
91 
92 static std::string
windows_default_tzname(void)93 windows_default_tzname (void)
94 {
95   const char *subkey =
96     "SYSTEM\\CurrentControlSet\\Control\\TimeZoneInformation";
97   constexpr size_t keysize {128};
98   HKEY key;
99   char key_name[keysize] {};
100   unsigned long tz_keysize = keysize;
101   if (RegOpenKeyExA (HKEY_LOCAL_MACHINE, subkey, 0,
102                      KEY_QUERY_VALUE, &key) == ERROR_SUCCESS)
103     {
104 	if (RegQueryValueExA (key, "TimeZoneKeyName", nullptr, nullptr,
105                                 (LPBYTE)key_name, &tz_keysize) != ERROR_SUCCESS)
106 	{
107 	    memset (key_name, 0, tz_keysize);
108         }
109       RegCloseKey (key);
110     }
111   return std::string(key_name);
112 }
113 
114 typedef   struct
115 {
116   LONG Bias;
117   LONG StandardBias;
118   LONG DaylightBias;
119   SYSTEMTIME StandardDate;
120   SYSTEMTIME DaylightDate;
121 } RegTZI;
122 
123 static time_zone_names
windows_tz_names(HKEY key)124 windows_tz_names (HKEY key)
125 {
126     /* The weird sizeof arg is because C++ won't find a type's
127      * element, just an object's.
128      */
129     constexpr auto s_size = sizeof (((TIME_ZONE_INFORMATION*)0)->StandardName);
130     char std_name[s_size];
131     unsigned long size = s_size;
132     if (RegQueryValueExA (key, "Std", NULL, NULL,
133 			  (LPBYTE)&(std_name), &size) != ERROR_SUCCESS)
134 	throw std::invalid_argument ("Registry contains no standard name.");
135 
136     constexpr auto d_size = sizeof (((TIME_ZONE_INFORMATION*)0)->DaylightName);
137     char dlt_name[d_size];
138     size = d_size;
139     if (RegQueryValueExA (key, "Dlt", NULL, NULL,
140 			  (LPBYTE)&(dlt_name), &size) != ERROR_SUCCESS)
141 	throw std::invalid_argument ("Registry contains no daylight name.");
142 
143     return time_zone_names (std_name, std_name, dlt_name, dlt_name);
144 }
145 
146 #define make_week_num(x)  static_cast<boost::date_time::nth_kday_of_month<boost::gregorian::date>::week_num>(x)
147 
148 static TZ_Ptr
zone_from_regtzi(const RegTZI & regtzi,time_zone_names names)149 zone_from_regtzi (const RegTZI& regtzi, time_zone_names names)
150 {
151     using ndate = boost::gregorian::nth_day_of_the_week_in_month;
152     using nth_day_rule = boost::local_time::nth_day_of_the_week_in_month_dst_rule;
153     /* Note that Windows runs its biases backwards from POSIX and
154      * boost::date_time: It's the value added to the local time to get
155      * GMT rather than the value added to GMT to get local time; for
156      * the same reason the DaylightBias is negative as one generally
157      * adds an hour less to the local time to get GMT. Biases are in
158      * minutes.
159      */
160     duration std_off (0, regtzi.StandardBias - regtzi.Bias, 0);
161     duration dlt_off (0, -regtzi.DaylightBias, 0);
162     duration start_time (regtzi.StandardDate.wHour, regtzi.StandardDate.wMinute,
163 			 regtzi.StandardDate.wSecond);
164     duration end_time (regtzi.DaylightDate.wHour, regtzi.DaylightDate.wMinute,
165 		       regtzi.DaylightDate.wSecond);
166     dst_offsets offsets (dlt_off, start_time, end_time);
167     auto std_week_num = make_week_num(regtzi.StandardDate.wDay);
168     auto dlt_week_num = make_week_num(regtzi.DaylightDate.wDay);
169     calc_rule_ptr dates;
170     if (regtzi.StandardDate.wMonth != 0)
171     {
172 	try
173 	{
174 	    ndate start (dlt_week_num, regtzi.DaylightDate.wDayOfWeek,
175 			 regtzi.DaylightDate.wMonth);
176 	    ndate end(std_week_num, regtzi.StandardDate.wDayOfWeek,
177 		      regtzi.StandardDate.wMonth);
178 	    dates.reset(new nth_day_rule (start, end));
179 	}
180 	catch (boost::gregorian::bad_month& err)
181 	{
182 	    PWARN("Caught Bad Month Exception. Daylight Bias: %ld  "
183 		  "Standard Month : %d  Daylight Month: %d",
184 		  regtzi.DaylightBias, regtzi.StandardDate.wMonth,
185 		  regtzi.DaylightDate.wMonth);
186 	}
187     }
188     return TZ_Ptr(new time_zone(names, std_off, offsets, dates));
189 }
190 
191 void
load_windows_dynamic_tz(HKEY key,time_zone_names names)192 TimeZoneProvider::load_windows_dynamic_tz (HKEY key, time_zone_names names)
193 {
194     DWORD first, last;
195 
196     try
197     {
198 	unsigned long size = sizeof first;
199 	if (RegQueryValueExA (key, "FirstEntry", NULL, NULL,
200 			      (LPBYTE) &first, &size) != ERROR_SUCCESS)
201 	    throw std::invalid_argument ("No first entry.");
202 
203 	size = sizeof last;
204 	if (RegQueryValueExA (key, "LastEntry", NULL, NULL,
205 			      (LPBYTE) &last, &size) != ERROR_SUCCESS)
206 	    throw std::invalid_argument ("No last entry.");
207 
208 	TZ_Ptr tz {};
209 	for (unsigned int year = first; year <= last; year++)
210 	{
211 	    auto s = to_string(year);
212 	    auto ystr = s.c_str();
213 	    RegTZI regtzi {};
214 	    size = sizeof regtzi;
215 	    auto err_val = RegQueryValueExA (key, ystr, NULL, NULL,
216 					     (LPBYTE) &regtzi, &size);
217 	    if (err_val != ERROR_SUCCESS)
218 	    {
219 		break;
220 	    }
221 	    tz = zone_from_regtzi (regtzi, names);
222 	    if (year == first)
223 		m_zone_vector.push_back (std::make_pair(0, tz));
224 	    m_zone_vector.push_back (std::make_pair(year, tz));
225 	}
226 	m_zone_vector.push_back (std::make_pair(max_year, tz));
227    }
228     catch (std::invalid_argument)
229     {
230 	RegCloseKey (key);
231 	throw;
232     }
233     catch (std::bad_alloc)
234     {
235 	RegCloseKey (key);
236 	throw;
237     }
238     RegCloseKey (key);
239 }
240 
241 void
load_windows_classic_tz(HKEY key,time_zone_names names)242 TimeZoneProvider::load_windows_classic_tz (HKEY key, time_zone_names names)
243 {
244     RegTZI regtzi {};
245     unsigned long size = sizeof regtzi;
246     try
247     {
248 	if (RegQueryValueExA (key, "TZI", NULL, NULL,
249 			      (LPBYTE) &regtzi, &size) == ERROR_SUCCESS)
250 	{
251 	    m_zone_vector.push_back(
252 		std::make_pair(max_year, zone_from_regtzi (regtzi, names)));
253 	}
254     }
255     catch (std::bad_alloc)
256     {
257 	RegCloseKey (key);
258 	throw;
259     }
260     RegCloseKey (key);
261 }
262 
263 void
load_windows_default_tz()264 TimeZoneProvider::load_windows_default_tz()
265 {
266     TIME_ZONE_INFORMATION tzi {};
267     GetTimeZoneInformation (&tzi);
268     RegTZI regtzi { tzi.Bias, tzi.StandardBias, tzi.DaylightBias,
269             tzi.StandardDate, tzi.DaylightDate };
270     using boost::locale::conv::utf_to_utf;
271     auto std_name = utf_to_utf<char>(tzi.StandardName,
272 				tzi.StandardName + sizeof(tzi.StandardName));
273     auto dlt_name = utf_to_utf<char>(tzi.DaylightName,
274 				tzi.DaylightName + sizeof(tzi.DaylightName));
275     time_zone_names names (std_name, std_name, dlt_name, dlt_name);
276     m_zone_vector.push_back(std::make_pair(max_year, zone_from_regtzi(regtzi, names)));
277 }
278 
TimeZoneProvider(const std::string & identifier)279 TimeZoneProvider::TimeZoneProvider (const std::string& identifier) :
280     m_zone_vector ()
281 {
282     HKEY key;
283     const std::string reg_key =
284 	"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones\\";
285 
286     auto key_name = (identifier.empty() ? windows_default_tzname () :
287 		     identifier);
288 
289     if (key_name.empty())
290     {
291         load_windows_default_tz();
292         return;
293     }
294     std::string subkey = reg_key + key_name;
295     if (RegOpenKeyExA (HKEY_LOCAL_MACHINE, subkey.c_str(), 0,
296 		       KEY_QUERY_VALUE, &key) != ERROR_SUCCESS)
297 	throw std::invalid_argument ("No TZ in registry named " + key_name);
298 
299     time_zone_names names {windows_tz_names (key)};
300     RegCloseKey (key);
301 
302     std::string subkey_dynamic = subkey + "\\Dynamic DST";
303     if (RegOpenKeyExA (HKEY_LOCAL_MACHINE, subkey_dynamic.c_str(), 0,
304 		       KEY_QUERY_VALUE, &key) == ERROR_SUCCESS)
305 	this->load_windows_dynamic_tz (key, names);
306     else if (RegOpenKeyExA (HKEY_LOCAL_MACHINE, subkey.c_str(), 0,
307 			    KEY_QUERY_VALUE, &key) == ERROR_SUCCESS)
308 	this->load_windows_classic_tz (key, names);
309     else
310 	throw std::invalid_argument ("No data for TZ " + key_name);
311 }
312 #elif PLATFORM(POSIX)
313 using std::to_string;
314 #include <istream>
315 #include <cstdlib>
316 
317 using boost::posix_time::ptime;
318 //To enable using Transition with different meanings for IANA files
319 //and for DSTRules.
320 namespace IANAParser
321 {
322     struct TZHead
323     {
324 	char magic[4];
325 	char version;
326 	uint8_t reserved[15];
327 	uint8_t ttisgmtcnt[4];
328 	uint8_t ttisstdcnt[4];
329 	uint8_t leapcnt[4];
330 	uint8_t timecnt[4];
331 	uint8_t typecnt[4];
332 	uint8_t charcnt[4];
333     };
334 
335     struct TTInfo
336     {
337 	int32_t gmtoff;
338 	uint8_t isdst;
339 	uint8_t abbrind;
340     };
341 
342     struct TZInfo
343     {
344 	TTInfo info;
345 	std::string name;
346 	bool isstd;
347 	bool isgmt;
348     };
349 
350     struct Transition
351     {
352 	int64_t timestamp;
353 	uint8_t index;
354     };
355 
356     static std::unique_ptr<char[]>
find_tz_file(const std::string & name)357     find_tz_file(const std::string& name)
358     {
359 	std::ifstream ifs;
360         auto tzname = name;
361         if (tzname.empty())
362             if (auto tzenv = getenv("TZ"))
363                 tzname = std::string(std::getenv("TZ"));
364         //std::cout << "Testing tzname " << tzname << "\n";
365         if (!tzname.empty())
366         {
367 //POSIX specifies that that identifier should begin with ':', but we
368 //should be liberal. If it's there, it's not part of the filename.
369 	    if (tzname[0] == ':')
370 		tzname.erase(tzname.begin());
371 	    if (tzname[0] == '/') //Absolute filename
372 	    {
373 		ifs.open(tzname, std::ios::in|std::ios::binary|std::ios::ate);
374 	    }
375 	    else
376 	    {
377 		const char* tzdir_c = std::getenv("TZDIR");
378 		std::string tzdir = tzdir_c ? tzdir_c : "/usr/share/zoneinfo";
379 //Note that we're not checking the filename.
380 		ifs.open(std::move(tzdir + "/" + tzname),
381 			 std::ios::in|std::ios::binary|std::ios::ate);
382 	    }
383 	}
384 
385 	if (! ifs.is_open())
386 	    throw std::invalid_argument("The timezone string failed to resolve to a valid filename");
387 	std::streampos filesize = ifs.tellg();
388 	std::unique_ptr<char[]>fileblock(new char[filesize]);
389 	ifs.seekg(0, std::ios::beg);
390 	ifs.read(fileblock.get(), filesize);
391 	ifs.close();
392 	return fileblock;
393     }
394 
395     using TZInfoVec = std::vector<TZInfo>;
396     using TZInfoIter = TZInfoVec::iterator;
397 
398     struct IANAParser
399     {
IANAParserIANAParser::IANAParser400 	IANAParser(const std::string& name) : IANAParser(find_tz_file(name)) {}
401 	IANAParser(std::unique_ptr<char[]>);
402 	std::vector<Transition>transitions;
403 	TZInfoVec tzinfo;
404 	int last_year;
405     };
406 
IANAParser(std::unique_ptr<char[]> fileblock)407     IANAParser::IANAParser(std::unique_ptr<char[]>fileblock)
408     {
409 	unsigned int fb_index = 0;
410 	TZHead tzh = *reinterpret_cast<TZHead*>(&fileblock[fb_index]);
411 	static constexpr int ttinfo_size = 6; //struct TTInfo gets padded
412 	last_year = 2037; //Constrained by 32-bit time_t.
413 	int transition_size = 4; // length of a transition time in the file
414 
415 	auto time_count = *(endian_swap(reinterpret_cast<uint32_t*>(tzh.timecnt)));
416 	auto type_count = *(endian_swap(reinterpret_cast<uint32_t*>(tzh.typecnt)));
417 	auto char_count = *(endian_swap(reinterpret_cast<uint32_t*>(tzh.charcnt)));
418 	auto isgmt_count = *(endian_swap(reinterpret_cast<uint32_t*>(tzh.ttisgmtcnt)));
419 	auto isstd_count = *(endian_swap(reinterpret_cast<uint32_t*>(tzh.ttisstdcnt)));
420 	auto leap_count = *(endian_swap(reinterpret_cast<uint32_t*>(tzh.leapcnt)));
421 	if ((tzh.version == '2' || tzh.version == '3'))
422 	{
423 	    fb_index = (sizeof(tzh) +
424 			(sizeof(uint32_t) + sizeof(uint8_t)) * time_count +
425 			ttinfo_size * type_count +
426 			sizeof(char) * char_count +
427 			sizeof(uint8_t) * isgmt_count +
428 			sizeof(uint8_t) * isstd_count +
429 			2 * sizeof(uint32_t) * leap_count);
430 
431 	    //This might change at some point in the probably very
432 	    //distant future.
433 	    tzh = *reinterpret_cast<TZHead*>(&fileblock[fb_index]);
434 	    last_year = 2499;
435 	    time_count = *(endian_swap(reinterpret_cast<uint32_t*>(tzh.timecnt)));
436 	    type_count = *(endian_swap(reinterpret_cast<uint32_t*>(tzh.typecnt)));
437 	    char_count = *(endian_swap(reinterpret_cast<uint32_t*>(tzh.charcnt)));
438 	    transition_size = 8;
439 	}
440 	fb_index += sizeof(tzh);
441 	auto start_index = fb_index;
442 	auto info_index_zero = start_index + time_count * transition_size;
443 	for(uint32_t index = 0; index < time_count; ++index)
444 	{
445 	    fb_index = start_index + index * transition_size;
446 	    auto info_index = info_index_zero + index;
447 	    if (transition_size  == 4)
448 	    {
449 		transitions.push_back(
450 		    {*(endian_swap(reinterpret_cast<int32_t*>(&fileblock[fb_index]))),
451 			    static_cast<uint8_t>(fileblock[info_index])});
452 	    }
453 	    else
454 	    {
455 		transitions.push_back(
456 		    {*(endian_swap(reinterpret_cast<int64_t*>(&fileblock[fb_index]))),
457 			    static_cast<uint8_t>(fileblock[info_index])});
458 	    }
459 	}
460 
461 	//Add in the tzinfo indexes consumed in the previous loop
462 	start_index = info_index_zero + time_count;
463 	auto abbrev = start_index + type_count * ttinfo_size;
464 	auto std_dist = abbrev + char_count;
465 	auto gmt_dist = std_dist + type_count;
466 	for(uint32_t index = 0; index < type_count; ++index)
467 	{
468 	    fb_index = start_index + index * ttinfo_size;
469 	    /* Use memcpy instead of static_cast to avoid memory alignment issues with chars */
470 	    TTInfo info{};
471 	    memcpy(&info, &fileblock[fb_index], ttinfo_size);
472 	    endian_swap(&info.gmtoff);
473 	    tzinfo.push_back(
474 		{info, &fileblock[abbrev + info.abbrind],
475 			fileblock[std_dist + index] != '\0',
476 			fileblock[gmt_dist + index] != '\0'});
477 	}
478 
479     }
480 }
481 
482 namespace DSTRule
483 {
484     using gregorian_date = boost::gregorian::date;
485     using IANAParser::TZInfoIter;
486     using ndate = boost::gregorian::nth_day_of_the_week_in_month;
487     using week_num =
488 	boost::date_time::nth_kday_of_month<boost::gregorian::date>::week_num;
489 
490     struct Transition
491     {
TransitionDSTRule::Transition492 	Transition() : month(1), dow(0), week(static_cast<week_num>(0)) {}
493 	Transition(gregorian_date date);
494 	bool operator==(const Transition& rhs) const noexcept;
495 	ndate get();
496 	boost::gregorian::greg_month month;
497 	boost::gregorian::greg_weekday dow;
498 	week_num week;
499     };
500 
Transition(gregorian_date date)501     Transition::Transition(gregorian_date date) :
502 	month(date.month()), dow(date.day_of_week()),
503 	week(static_cast<week_num>((6 + date.day() - date.day_of_week()) / 7))
504     {}
505 
506     bool
operator ==(const Transition & rhs) const507     Transition::operator==(const Transition& rhs) const noexcept
508     {
509 	return (month == rhs.month && dow == rhs.dow && week == rhs.week);
510     }
511 
512     ndate
get()513     Transition::get()
514     {
515 	return ndate(week, dow, month);
516     }
517 
518     struct DSTRule
519     {
520 	DSTRule();
521 	DSTRule(TZInfoIter info1, TZInfoIter info2,
522 		ptime date1, ptime date2);
523 	bool operator==(const DSTRule& rhs) const noexcept;
524 	bool operator!=(const DSTRule& rhs) const noexcept;
525 	Transition to_std;
526 	Transition to_dst;
527 	duration to_std_time;
528 	duration to_dst_time;
529 	TZInfoIter std_info;
530 	TZInfoIter dst_info;
531     };
532 
DSTRule()533     DSTRule::DSTRule() : to_std(), to_dst(), to_std_time {}, to_dst_time {},
534 	std_info (), dst_info () {};
535 
DSTRule(TZInfoIter info1,TZInfoIter info2,ptime date1,ptime date2)536     DSTRule::DSTRule (TZInfoIter info1, TZInfoIter info2,
537 		      ptime date1, ptime date2) :
538 	to_std(date1.date()), to_dst(date2.date()),
539 	to_std_time(date1.time_of_day()), to_dst_time(date2.time_of_day()),
540 	std_info(info1), dst_info(info2)
541     {
542 	if (info1->info.isdst == info2->info.isdst)
543 	    throw(std::invalid_argument("Both infos have the same dst value."));
544 	if (info1->info.isdst && !info2->info.isdst)
545 	{
546 	    std::swap(to_std, to_dst);
547 	    std::swap(to_std_time, to_dst_time);
548 	    std::swap(std_info, dst_info);
549 	}
550 
551         /* Documentation notwithstanding, the date-time rules are
552          * looking for local time (wall clock to use the RFC 8538
553          * definition) values.
554          *
555          * The TZ Info contains two fields, isstd and isgmt (renamed
556          * to isut in newer versions of tzinfo). In theory if both are
557          * 0 the transition times represent wall-clock times,
558          * i.e. time stamps in the respective time zone's local time
559          * at the moment of the transition. If isstd is 1 then the
560          * representation is always in standard time instead of
561          * daylight time; this is significant for dst->std
562          * transitions. If isgmt/isut is one then isstd must also be
563          * set and the transition time is in UTC.
564          *
565          * In practice it seems that the timestamps are always in UTC
566          * so the isgmt/isut flag isn't meaningful. The times always
567          * need to have the utc offset added to them to make the
568          * transition occur at the right time; the isstd flag
569          * determines whether that should be the standard offset or
570          * the daylight offset for the daylight->standard transition.
571          */
572 
573         to_dst_time += boost::posix_time::seconds(std_info->info.gmtoff);
574         if (std_info->isstd) //if isstd always use standard time
575             to_std_time += boost::posix_time::seconds(std_info->info.gmtoff);
576         else
577             to_std_time += boost::posix_time::seconds(dst_info->info.gmtoff);
578 
579     }
580 
581     bool
operator ==(const DSTRule & rhs) const582     DSTRule::operator==(const DSTRule& rhs) const noexcept
583     {
584 	return (to_std == rhs.to_std &&
585 		to_dst == rhs.to_dst &&
586 		to_std_time == rhs.to_std_time &&
587 		to_dst_time == rhs.to_dst_time &&
588 		std_info == rhs.std_info &&
589 		dst_info == rhs.dst_info);
590     }
591 
592     bool
operator !=(const DSTRule & rhs) const593     DSTRule::operator!=(const DSTRule& rhs) const noexcept
594     {
595 	return ! operator==(rhs);
596     }
597 }
598 
599 static TZ_Entry
zone_no_dst(int year,IANAParser::TZInfoIter std_info)600 zone_no_dst(int year, IANAParser::TZInfoIter std_info)
601 {
602     time_zone_names names(std_info->name, std_info->name, "", "");
603     duration std_off(0, 0, std_info->info.gmtoff);
604     dst_offsets offsets({0, 0, 0}, {0, 0, 0}, {0, 0, 0});
605     boost::local_time::dst_calc_rule_ptr calc_rule(nullptr);
606     TZ_Ptr tz(new time_zone(names, std_off, offsets, calc_rule));
607     return std::make_pair(year, tz);
608 }
609 
610 static TZ_Entry
zone_from_rule(int year,DSTRule::DSTRule rule)611 zone_from_rule(int year, DSTRule::DSTRule rule)
612 {
613     using boost::gregorian::partial_date;
614     using boost::local_time::partial_date_dst_rule;
615     using nth_day_rule =
616 	boost::local_time::nth_day_of_the_week_in_month_dst_rule;
617 
618     time_zone_names names(rule.std_info->name, rule.std_info->name,
619 			  rule.dst_info->name, rule.dst_info->name);
620     duration std_off(0, 0, rule.std_info->info.gmtoff);
621     duration dlt_off(0, 0,
622 		     rule.dst_info->info.gmtoff - rule.std_info->info.gmtoff);
623     dst_offsets offsets(dlt_off, rule.to_dst_time, rule.to_std_time);
624     calc_rule_ptr dates(new nth_day_rule(rule.to_dst.get(), rule.to_std.get()));
625     TZ_Ptr tz(new time_zone(names, std_off, offsets, dates));
626     return std::make_pair(year, tz);
627 }
628 
629 void
parse_file(const std::string & tzname)630 TimeZoneProvider::parse_file(const std::string& tzname)
631 {
632     IANAParser::IANAParser parser(tzname);
633     using boost::posix_time::hours;
634     const auto one_year = hours(366 * 24); //Might be a leap year.
635     auto last_info = std::find_if(parser.tzinfo.begin(), parser.tzinfo.end(),
636                                   [](IANAParser::TZInfo tz)
637                                   {return !tz.info.isdst;});
638     auto last_time = ptime();
639     DSTRule::DSTRule last_rule;
640     using boost::gregorian::date;
641     using boost::posix_time::ptime;
642     using boost::posix_time::time_duration;
643     for (auto txi = parser.transitions.begin();
644          txi != parser.transitions.end(); ++txi)
645     {
646         auto this_info = parser.tzinfo.begin() + txi->index;
647 //Can't use boost::posix_date::from_time_t() constructor because it
648 //silently casts the time_t to an int32_t.
649         auto this_time = ptime(date(1970, 1, 1),
650                                time_duration(txi->timestamp / 3600, 0,
651                                              txi->timestamp % 3600));
652         /* Note: The "get" function retrieves the last zone with a
653          * year *earlier* than the requested year: Zone periods run
654          * from the saved year to the beginning year of the next zone.
655          */
656         try
657         {
658             auto this_year = this_time.date().year();
659             //Initial case
660             if (last_time.is_not_a_date_time())
661             {
662                 m_zone_vector.push_back(zone_no_dst(this_year - 1, last_info));
663                 m_zone_vector.push_back(zone_no_dst(this_year, this_info));
664             }
665             // No change in is_dst means a permanent zone change.
666             else if (last_info->info.isdst == this_info->info.isdst)
667             {
668                 m_zone_vector.push_back(zone_no_dst(this_year, this_info));
669             }
670             /* If there have been no transitions in at least a year
671              * then we need to create a no-DST rule with last_info to
672              * reflect the frozen timezone.
673              */
674             else if (this_time - last_time > one_year)
675             {
676                 auto year = last_time.date().year();
677                 if (m_zone_vector.back().first == year)
678                     year = year + 1; // no operator ++ or +=, sigh.
679                 m_zone_vector.push_back(zone_no_dst(year, last_info));
680             }
681             /* It's been less than a year, so it's probably a DST
682              * cycle. This consumes two transitions so we want only
683              * the return-to-standard-time one to make a DST rule.
684              */
685             else if (!this_info->info.isdst)
686             {
687                 DSTRule::DSTRule new_rule(last_info, this_info,
688                                           last_time, this_time);
689                 if (new_rule != last_rule)
690                 {
691                     last_rule = new_rule;
692                     auto year = last_time.date().year();
693                     m_zone_vector.push_back(zone_from_rule(year, new_rule));
694                 }
695             }
696         }
697         catch(const boost::gregorian::bad_year& err)
698         {
699             continue;
700         }
701         last_time = this_time;
702         last_info = this_info;
703     }
704 /* if the transitions end before the end of the zoneinfo coverage
705  * period then the zone rescinded DST and we need a final no-dstzone.
706  */
707     if (last_time.is_not_a_date_time())
708         m_zone_vector.push_back(zone_no_dst(max_year, last_info));
709     else if (last_time.date().year() < parser.last_year)
710         m_zone_vector.push_back(zone_no_dst(last_time.date().year(), last_info));
711 }
712 
713 bool
construct(const std::string & tzname)714 TimeZoneProvider::construct(const std::string& tzname)
715 {
716     try
717     {
718         parse_file(tzname);
719     }
720     catch(const std::invalid_argument& err)
721     {
722         try
723         {
724             TZ_Ptr zone(new PTZ(tzname));
725             m_zone_vector.push_back(std::make_pair(max_year, zone));
726         }
727         catch(std::exception& err)
728         {
729             return false;
730         }
731     }
732     return true;
733 }
734 
TimeZoneProvider(const std::string & tzname)735 TimeZoneProvider::TimeZoneProvider(const std::string& tzname) :  m_zone_vector {}
736 {
737     if(construct(tzname))
738         return;
739     DEBUG("%s invalid, trying TZ environment variable.\n", tzname.c_str());
740     const char* tz_env = getenv("TZ");
741     if(tz_env && construct(tz_env))
742         return;
743     DEBUG("No valid $TZ, resorting to /etc/localtime.\n");
744     try
745     {
746         parse_file("/etc/localtime");
747     }
748     catch(const std::invalid_argument& env)
749     {
750         DEBUG("/etc/localtime invalid, resorting to GMT.");
751         TZ_Ptr zone(new PTZ("UTC0"));
752         m_zone_vector.push_back(std::make_pair(max_year, zone));
753     }
754 }
755 #endif
756 
757 
758 TZ_Ptr
get(int year) const759 TimeZoneProvider::get(int year) const noexcept
760 {
761     if (m_zone_vector.empty())
762         return TZ_Ptr(new PTZ("UTC0"));
763     auto iter = find_if(m_zone_vector.rbegin(), m_zone_vector.rend(),
764 			[=](TZ_Entry e) { return e.first <= year; });
765     if (iter == m_zone_vector.rend())
766             return m_zone_vector.front().second;
767     return iter->second;
768 }
769 
770 void
dump() const771 TimeZoneProvider::dump() const noexcept
772 {
773     for (auto zone : m_zone_vector)
774 	std::cout << zone.first << ": " << zone.second->to_posix_string() << "\n";
775 }
776