Reactos
1//
2// tzset.cpp
3//
4// Copyright (c) Microsoft Corporation. All rights reserved.
5//
6// Defines the _tzset() function which updates the global time zone state, and
7// the _isindst() function, which tests whether a time is in Daylight Savings
8// Time or not.
9//
10#include <corecrt_internal_time.h>
11#include <locale.h>
12
13
14
15_DEFINE_SET_FUNCTION(_set_daylight, int, _daylight)
16_DEFINE_SET_FUNCTION(_set_dstbias, long, _dstbias )
17_DEFINE_SET_FUNCTION(_set_timezone, long, _timezone)
18
19
20
21// Pointer to a saved copy of the TZ value obtained in the previous call to the
22// tzset functions, if one is available:
23static wchar_t* last_wide_tz = nullptr;
24
25// If the time zone was last updated by calling the system API, then the tz_info
26// variable contains the time zone information and tz_api_used is set to true.
27static int tz_api_used;
28static TIME_ZONE_INFORMATION tz_info;
29
30static __crt_state_management::dual_state_global<long> tzset_init_state;
31
32namespace
33{
34 // Structure used to represent DST transition date/times:
35 struct transitiondate
36 {
37 int yr; // year of interest
38 int yd; // day of year
39 int ms; // milli-seconds in the day
40 };
41
42 enum class date_type
43 {
44 absolute_date,
45 day_in_month
46 };
47
48 enum class transition_type
49 {
50 start_of_dst,
51 end_of_dst
52 };
53
54 size_t const local_env_buffer_size = 256;
55 int const milliseconds_per_day = 24 * 60 * 60 * 1000;
56}
57
58// DST start and end structures:
59static transitiondate dststart = { -1, 0, 0 };
60static transitiondate dstend = { -1, 0, 0 };
61
62
63
64//-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
65//
66// The _tzset() family of functions
67//
68//-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
69// Gets the value of the TZ environment variable. If there is no TZ environment
70// variable or if we do not have access to the environment, nullptr is returned.
71// If the value of the TZ variable fits into the local_buffer, it is stored there
72// and a pointer to the local_buffer is returned. Otherwise, a buffer is
73// dynamically allocated, the value is stored into that buffer, and a pointer to
74// that buffer is returned. In this case, the caller is responsible for freeing
75// the buffer.
76static wchar_t* get_tz_environment_variable(wchar_t (&local_buffer)[local_env_buffer_size]) throw()
77{
78 size_t required_length;
79 errno_t const status = _wgetenv_s(&required_length, local_buffer, local_env_buffer_size, L"TZ");
80 if (status == 0)
81 {
82 return local_buffer;
83 }
84
85 if (status != ERANGE)
86 {
87 return nullptr;
88 }
89
90 __crt_unique_heap_ptr<wchar_t> dynamic_buffer(_malloc_crt_t(wchar_t, required_length));
91 if (dynamic_buffer.get() == nullptr)
92 {
93 return nullptr;
94 }
95
96 size_t actual_length;
97 if (_wgetenv_s(&actual_length, dynamic_buffer.get(), required_length, L"TZ") != 0)
98 {
99 return nullptr;
100 }
101
102 return dynamic_buffer.detach();
103}
104
105static void __cdecl tzset_os_copy_to_tzname(const wchar_t * const timezone_name, wchar_t * const wide_tzname, char * const narrow_tzname, unsigned int const code_page)
106{
107 // Maximum time zone name from OS is 32 characters long
108 // (see https://docs.microsoft.com/en-us/windows/desktop/api/timezoneapi/ns-timezoneapi-_time_zone_information)
109 _ERRCHECK(wcsncpy_s(wide_tzname, _TZ_STRINGS_SIZE, timezone_name, 32));
110
111 // Invalid characters are replaced by closest approximation or default character.
112 // On other failure, leave narrow tzname blank.
113 __acrt_WideCharToMultiByte(
114 code_page,
115 0,
116 timezone_name,
117 -1,
118 narrow_tzname,
119 _TZ_STRINGS_SIZE, // Passing -1 as source size, so null terminator included.
120 nullptr,
121 nullptr
122 );
123}
124
125// Handles the _tzset if and only if there is no TZ environment variable. In
126// this case, we attempt to use the time zone information from the system.
127static void __cdecl tzset_from_system_nolock() throw()
128{
129 _BEGIN_SECURE_CRT_DEPRECATION_DISABLE
130 char** tzname = _tzname;
131 wchar_t** wide_tzname = __wide_tzname();
132 _END_SECURE_CRT_DEPRECATION_DISABLE
133
134 long timezone = 0;
135 int daylight = 0;
136 long dstbias = 0;
137 _ERRCHECK(_get_timezone(&timezone));
138 _ERRCHECK(_get_daylight(&daylight));
139 _ERRCHECK(_get_dstbias (&dstbias ));
140
141 // If there is a last_wide_tz already, discard it:
142 _free_crt(last_wide_tz);
143 last_wide_tz = nullptr;
144
145 if (GetTimeZoneInformation(&tz_info) != 0xFFFFFFFF)
146 {
147 // Record that the API was used:
148 tz_api_used = 1;
149
150 // Derive _timezone value from Bias and StandardBias fields.
151 timezone = tz_info.Bias * 60;
152
153 if (tz_info.StandardDate.wMonth != 0)
154 timezone += tz_info.StandardBias * 60;
155
156 // Check to see if there is a daylight time bias. Since the StandardBias
157 // has been added into _timezone, it must be compensated for in the
158 // value computed for _dstbias:
159 if (tz_info.DaylightDate.wMonth != 0 && tz_info.DaylightBias != 0)
160 {
161 daylight = 1;
162 dstbias = (tz_info.DaylightBias - tz_info.StandardBias) * 60;
163 }
164 else
165 {
166 daylight = 0;
167
168 // Set the bias to 0 because GetTimeZoneInformation may return
169 // TIME_ZONE_ID_DAYLIGHT even though there is no DST (e.g., in NT
170 // 3.51, this can happen if automatic DST adjustment is disabled
171 // in the Control Panel.
172 dstbias = 0;
173 }
174
175 memset(wide_tzname[0], 0, _TZ_STRINGS_SIZE * sizeof(wchar_t));
176 memset(wide_tzname[1], 0, _TZ_STRINGS_SIZE * sizeof(wchar_t));
177 memset(tzname[0], 0, _TZ_STRINGS_SIZE);
178 memset(tzname[1], 0, _TZ_STRINGS_SIZE);
179
180 // Try to grab the name strings for both the time zone and the daylight
181 // zone. Note the wide character strings in tz_info must be converted
182 // to multibyte character strings. The locale code page must be used
183 // for this. Note that if setlocale() has not yet been called with
184 // LC_ALL or LC_CTYPE, then the code page will be 0, which is CP_ACP,
185 // so we will use the host's default ANSI code page.
186 //
187 // CRT_REFACTOR TODO We use the current locale for this transformation.
188 // If per-thread locale has been enabled for this thread, then we'll be
189 // using this thread's locale to update a global variable that is
190 // accessed from multiple threads. Does the time zone information also
191 // need to be stored per-thread?
192 unsigned const code_page = ___lc_codepage_func();
193
194 tzset_os_copy_to_tzname(tz_info.StandardName, wide_tzname[0], tzname[0], code_page);
195 tzset_os_copy_to_tzname(tz_info.DaylightName, wide_tzname[1], tzname[1], code_page);
196 }
197
198 _set_timezone(timezone);
199 _set_daylight(daylight);
200 _set_dstbias(dstbias);
201}
202
203static void __cdecl tzset_env_copy_to_tzname(const wchar_t * const tz_env, wchar_t * const wide_tzname, char * const narrow_tzname, rsize_t const tzname_length)
204{
205 _ERRCHECK(wcsncpy_s(wide_tzname, _TZ_STRINGS_SIZE, tz_env, tzname_length));
206
207 // Historically when getting _tzname via TZ, the narrow environment was used to populate _tzname when getting _tzname.
208 // The narrow environment is always encoded in the ACP (so _tzname was encoded in the ACP when coming from TZ), but
209 // when getting _tzname from the OS, the current active code page (set via setlocale()) was used instead.
210 // To maintain behavior compatibility, we remain intentionally inconsistent with
211 // how _tzname is generated when getting time zone information from the OS by explicitly encoding with the ACP.
212 // UTF-8 mode is opt-in, so we can correct this inconsistency when the current code page is UTF-8.
213
214 // Invalid characters are replaced by closest approximation or default character.
215 // On other failure, simply leave _tzname blank.
216 __acrt_WideCharToMultiByte(
217 __acrt_get_utf8_acp_compatibility_codepage(),
218 0,
219 wide_tzname,
220 static_cast<int>(tzname_length),
221 narrow_tzname,
222 _TZ_STRINGS_SIZE - 1, // Leave room for null terminator
223 nullptr,
224 nullptr);
225}
226
227static void __cdecl tzset_from_environment_nolock(_In_z_ wchar_t* tz_env) throw()
228{
229 _BEGIN_SECURE_CRT_DEPRECATION_DISABLE
230 char** tzname = _tzname;
231 wchar_t** wide_tzname = __wide_tzname();
232 _END_SECURE_CRT_DEPRECATION_DISABLE
233
234 long timezone = 0;
235 int daylight = 0;
236 _ERRCHECK(_get_timezone(&timezone));
237 _ERRCHECK(_get_daylight(&daylight));
238
239 // Check to see if the TZ value is unchanged from an earlier call to this
240 // function. If it hasn't changed, we have no work to do:
241 if (last_wide_tz != nullptr && wcscmp(tz_env, last_wide_tz) == 0)
242 {
243 return;
244 }
245
246 // Update the global last_wide_tz variable:
247 auto new_wide_tz = _malloc_crt_t(wchar_t, wcslen(tz_env) + 1);
248 if (!new_wide_tz)
249 {
250 return;
251 }
252
253 _free_crt(last_wide_tz);
254 last_wide_tz = new_wide_tz.detach();
255
256 _ERRCHECK(wcscpy_s(last_wide_tz, wcslen(tz_env) + 1, tz_env));
257
258 // Process TZ value and update _tzname, _timezone and _daylight.
259 memset(wide_tzname[0], 0, _TZ_STRINGS_SIZE * sizeof(wchar_t));
260 memset(wide_tzname[1], 0, _TZ_STRINGS_SIZE * sizeof(wchar_t));
261 memset(tzname[0], 0, _TZ_STRINGS_SIZE);
262 memset(tzname[1], 0, _TZ_STRINGS_SIZE);
263
264 rsize_t const tzname_length = 3;
265
266 // Copy standard time zone name (index 0)
267 tzset_env_copy_to_tzname(tz_env, wide_tzname[0], tzname[0], tzname_length);
268
269 // Skip first few characters if present.
270 for (rsize_t i = 0; i < tzname_length; ++i)
271 {
272 if (*tz_env)
273 {
274 ++tz_env;
275 }
276 }
277
278 // The time difference is of the form:
279 // [+|-]hh[:mm[:ss]]
280 // Check for the minus sign first:
281 bool const is_negative_difference = *tz_env == L'-';
282 if (is_negative_difference)
283 {
284 ++tz_env;
285 }
286
287 wchar_t * dummy;
288 int const decimal_base = 10;
289
290 // process, then skip over, the hours
291 timezone = wcstol(tz_env, &dummy, decimal_base) * 3600;
292 while (*tz_env == '+' || (*tz_env >= L'0' && *tz_env <= L'9'))
293 {
294 ++tz_env;
295 }
296
297
298 // Check if minutes were specified:
299 if (*tz_env == L':')
300 {
301 // Process, then skip over, the minutes
302 timezone += wcstol(++tz_env, &dummy, decimal_base) * 60;
303 while (*tz_env >= L'0' && *tz_env <= L'9')
304 {
305 ++tz_env;
306 }
307
308 // Check if seconds were specified:
309 if (*tz_env == L':')
310 {
311 // Process, then skip over, the seconds:
312 timezone += wcstol(++tz_env, &dummy, decimal_base);
313 while (*tz_env >= L'0' && *tz_env <= L'9')
314 {
315 ++tz_env;
316 }
317 }
318 }
319
320 if (is_negative_difference)
321 {
322 timezone = -timezone;
323 }
324
325 // Finally, check for a DST zone suffix:
326 daylight = *tz_env ? 1 : 0;
327
328 if (daylight)
329 {
330 // Copy daylight time zone name (index 1)
331 tzset_env_copy_to_tzname(tz_env, wide_tzname[1], tzname[1], tzname_length);
332 }
333
334 _set_timezone(timezone);
335 _set_daylight(daylight);
336}
337
338static void __cdecl tzset_nolock() throw()
339{
340 // Clear the flag indicated whether GetTimeZoneInformation was used.
341 tz_api_used = 0;
342
343 // Set year fields of dststart and dstend structures to -1 to ensure
344 // they are recomputed as after this
345 dststart.yr = dstend.yr = -1;
346
347 // Get the value of the TZ environment variable:
348 wchar_t local_env_buffer[local_env_buffer_size];
349 wchar_t* const tz_env = get_tz_environment_variable(local_env_buffer);
350
351 // If the buffer ended up being dynamically allocated, make sure we
352 // clean it up before we return:
353 __crt_unique_heap_ptr<wchar_t> tz_env_cleanup(tz_env == local_env_buffer
354 ? nullptr
355 : tz_env);
356
357 // If the environment variable is not available for whatever reason, update
358 // without using the environment (note that unless the Desktop CRT is loaded
359 // and we have access to non-MSDK APIs, we will always tak this path).
360 if (tz_env == nullptr || tz_env[0] == '\0')
361 return tzset_from_system_nolock();
362
363 return tzset_from_environment_nolock(tz_env);
364}
365
366
367
368// Sets the time zone information and calculates whether we are currently in
369// Daylight Savings Time. This reads the TZ environment variable, if that
370// variable exists and can be read by the process; otherwise, the system is
371// queried for the current time zone state. The _daylight, _timezone, and
372// _tzname global variables are updated accordingly.
373extern "C" void __cdecl _tzset()
374{
375 __acrt_lock(__acrt_time_lock);
376 __try
377 {
378 tzset_nolock();
379 }
380 __finally
381 {
382 __acrt_unlock(__acrt_time_lock);
383 }
384 __endtry
385}
386
387
388
389// This function may be called to ensure that the time zone information ha sbeen
390// set at least once. If the time zone information has not yet been set, this
391// function sets it.
392extern "C" void __cdecl __tzset()
393{
394 auto const first_time = tzset_init_state.dangerous_get_state_array() + __crt_state_management::get_current_state_index();
395
396 if (__crt_interlocked_read(first_time) != 0)
397 {
398 return;
399 }
400
401 __acrt_lock(__acrt_time_lock);
402 __try
403 {
404 if (__crt_interlocked_read(first_time) != 0)
405 {
406 __leave;
407 }
408
409 tzset_nolock();
410
411 _InterlockedIncrement(first_time);
412 }
413 __finally
414 {
415 __acrt_unlock(__acrt_time_lock);
416 }
417 __endtry
418}
419
420
421
422//-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
423//
424// The _isindst() family of functions
425//
426//-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
427// Converts the format of a transition date specification to a value of a
428// transitiondate structure. The dststart and dstend global variables are
429// filled in with the converted date.
430static void __cdecl cvtdate(
431 transition_type const trantype, // start or end of DST
432 date_type const datetype, // Day-in-month or absolute date
433 int const year, // Year, as an offset from 1900
434 int const month, // Month, where 0 is January
435 int const week, // Week of month, if datetype is day-in-month
436 int const dayofweek, // Day of week, if datetype is day-in-month
437 int const date, // Date of month (1 - 31)
438 int const hour, // Hours (0 - 23)
439 int const min, // Minutes (0 - 59)
440 int const sec, // Seconds (0 - 59)
441 int const msec // Milliseconds (0 - 999)
442 ) throw()
443{
444 int yearday;
445 int monthdow;
446 long dstbias = 0;
447
448 if (datetype == date_type::day_in_month)
449 {
450 // Figure out the year-day of the start of the month:
451 yearday = 1 + (__crt_time_is_leap_year(year)
452 ? _lpdays[month - 1]
453 : _days[month - 1]);
454
455 // Figureo ut the day of the week of the start of the month:
456 monthdow = (yearday + ((year - 70) * 365) +
457 __crt_time_elapsed_leap_years(year) + _BASE_DOW) % 7;
458
459 // Figure out the year-day of the transition date:
460 if (monthdow <= dayofweek)
461 yearday += (dayofweek - monthdow) + (week - 1) * 7;
462 else
463 yearday += (dayofweek - monthdow) + week * 7;
464
465 // We may have to adjust the calculation above if week == 5 (meaning the
466 // last instance of the day in the month). Check if the year falls
467 // beyond after month and adjust accordingly:
468 int const days_to_compare = __crt_time_is_leap_year(year)
469 ? _lpdays[month]
470 : _days[month];
471
472 if (week == 5 && yearday > days_to_compare)
473 {
474 yearday -= 7;
475 }
476 }
477 else
478 {
479 yearday = __crt_time_is_leap_year(year)
480 ? _lpdays[month - 1]
481 : _days[month - 1];
482
483 yearday += date;
484 }
485
486 if (trantype == transition_type::start_of_dst)
487 {
488 dststart.yd = yearday;
489 dststart.ms = msec + (1000 * (sec + 60 * (min + 60 * hour)));
490
491 // Set the year field of dststart so that unnecessary calls to cvtdate()
492 // may be avoided:
493 dststart.yr = year;
494 }
495 else // end_of_dst
496 {
497 dstend.yd = yearday;
498 dstend.ms = msec + (1000 * (sec + 60 * (min + 60 * hour)));
499
500 // The converted date is still a DST date. We must convert to a standard
501 // (local) date while being careful the millisecond field does not
502 // overflow or underflow
503 _ERRCHECK(_get_dstbias(&dstbias));
504 dstend.ms += (dstbias * 1000);
505 if (dstend.ms < 0)
506 {
507 dstend.ms += milliseconds_per_day;
508 dstend.yd--;
509 }
510 else if (dstend.ms >= milliseconds_per_day)
511 {
512 dstend.ms -= milliseconds_per_day;
513 dstend.yd++;
514 }
515
516 // Set the year field of dstend so that unnecessary calls to cvtdate()
517 // may be avoided:
518 dstend.yr = year;
519 }
520
521 return;
522}
523
524
525
526// Implementation Details: Note that there are two ways that the Daylight
527// Savings Time transition data may be returned by GetTimeZoneInformation. The
528// first is a day-in-month format, which is similar to what is used in the USA.
529// The transition date is given as the n'th occurrence of a specified day of the
530// week in a specified month. The second is as an absolute date. The two cases
531// are distinguished by the value of the wYear field of the SYSTEMTIME structure
532// (zero denotes a day-in-month format).
533static int __cdecl _isindst_nolock(tm* const tb) throw()
534{
535 int daylight = 0;
536 _ERRCHECK(_get_daylight(&daylight));
537 if (daylight == 0)
538 return 0;
539
540 // Compute (or recompute) the transition dates for Daylight Savings Time
541 // if necessary. The yr fields of dststart and dstend are compared to the
542 // year of interest to determine necessity.
543 if (tb->tm_year != dststart.yr || tb->tm_year != dstend.yr)
544 {
545 if (tz_api_used)
546 {
547 // Convert the start of daylight savings time to dststart:
548 if (tz_info.DaylightDate.wYear == 0)
549 {
550 cvtdate(
551 transition_type::start_of_dst,
552 date_type::day_in_month,
553 tb->tm_year,
554 tz_info.DaylightDate.wMonth,
555 tz_info.DaylightDate.wDay,
556 tz_info.DaylightDate.wDayOfWeek,
557 0,
558 tz_info.DaylightDate.wHour,
559 tz_info.DaylightDate.wMinute,
560 tz_info.DaylightDate.wSecond,
561 tz_info.DaylightDate.wMilliseconds);
562 }
563 else
564 {
565 cvtdate(
566 transition_type::start_of_dst,
567 date_type::absolute_date,
568 tb->tm_year,
569 tz_info.DaylightDate.wMonth,
570 0,
571 0,
572 tz_info.DaylightDate.wDay,
573 tz_info.DaylightDate.wHour,
574 tz_info.DaylightDate.wMinute,
575 tz_info.DaylightDate.wSecond,
576 tz_info.DaylightDate.wMilliseconds);
577 }
578
579 // Convert start of standard time to dstend:
580 if (tz_info.StandardDate.wYear == 0)
581 {
582 cvtdate(
583 transition_type::end_of_dst,
584 date_type::day_in_month,
585 tb->tm_year,
586 tz_info.StandardDate.wMonth,
587 tz_info.StandardDate.wDay,
588 tz_info.StandardDate.wDayOfWeek,
589 0,
590 tz_info.StandardDate.wHour,
591 tz_info.StandardDate.wMinute,
592 tz_info.StandardDate.wSecond,
593 tz_info.StandardDate.wMilliseconds);
594 }
595 else
596 {
597 cvtdate(
598 transition_type::end_of_dst,
599 date_type::absolute_date,
600 tb->tm_year,
601 tz_info.StandardDate.wMonth,
602 0,
603 0,
604 tz_info.StandardDate.wDay,
605 tz_info.StandardDate.wHour,
606 tz_info.StandardDate.wMinute,
607 tz_info.StandardDate.wSecond,
608 tz_info.StandardDate.wMilliseconds);
609 }
610 }
611 else
612 {
613 // The GetTimeZoneInformation API was not used, or failed. We use
614 // the USA Daylight Savings Time rules as a fallback.
615 int startmonth = 3; // March
616 int startweek = 2; // Second week
617 int endmonth = 11;// November
618 int endweek = 1; // First week
619
620 // The rules changed in 2007:
621 if (107 > tb->tm_year)
622 {
623 startmonth = 4; // April
624 startweek = 1; // first week
625 endmonth = 10;// October
626 endweek = 5; // last week
627 }
628
629 cvtdate(
630 transition_type::start_of_dst,
631 date_type::day_in_month,
632 tb->tm_year,
633 startmonth,
634 startweek,
635 0, // Sunday
636 0,
637 2, // 02:00 (2 AM)
638 0,
639 0,
640 0);
641
642 cvtdate(
643 transition_type::end_of_dst,
644 date_type::day_in_month,
645 tb->tm_year,
646 endmonth,
647 endweek,
648 0, // Sunday
649 0,
650 2, // 02:00 (2 AM)
651 0,
652 0,
653 0);
654 }
655 }
656
657 // Handle simple cases first:
658 if (dststart.yd < dstend.yd)
659 {
660 // Northern hemisphere ordering:
661 if (tb->tm_yday < dststart.yd || tb->tm_yday > dstend.yd)
662 return 0;
663
664 if (tb->tm_yday > dststart.yd && tb->tm_yday < dstend.yd)
665 return 1;
666 }
667 else
668 {
669 // Southern hemisphere ordering:
670 if (tb->tm_yday < dstend.yd || tb->tm_yday > dststart.yd)
671 return 1;
672
673 if (tb->tm_yday > dstend.yd && tb->tm_yday < dststart.yd)
674 return 0;
675 }
676
677 long const ms = 1000 * (tb->tm_sec + 60 * tb->tm_min + 3600 * tb->tm_hour);
678
679 if (tb->tm_yday == dststart.yd)
680 {
681
682 return ms >= dststart.ms ? 1 : 0;
683 }
684 else
685 {
686 return ms < dstend.ms ? 1 : 0;
687 }
688}
689
690
691
692// Tests if the time represented by the tm structure falls in Daylight Savings
693// Time or not. The Daylight Savings Time rules are obtained from the operating
694// system if GetTimeZoneInformation was used by _tzset() to obtain the time zone
695// information; otherwise, the USA Daylight Savings Time rules (post-1986) are
696// used.
697//
698// Returns 1 if the time is in Daylight Savings Time; returns 0 otherwise.
699extern "C" int __cdecl _isindst(tm* const tb)
700{
701 int retval = 0;
702
703 __acrt_lock(__acrt_time_lock);
704 __try
705 {
706 retval = _isindst_nolock(tb);
707 }
708 __finally
709 {
710 __acrt_unlock(__acrt_time_lock);
711 }
712 __endtry
713
714 return retval;
715}