summaryrefslogtreecommitdiffstats
path: root/lib/time_rz.c
blob: 5d85963c9ed9c912d6ebf56bc8b8ad2da89989a9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
/* Time zone functions such as tzalloc and localtime_rz

   Copyright 2015-2020 Free Software Foundation, Inc.

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 3, or (at your option)
   any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License along
   with this program; if not, see <https://www.gnu.org/licenses/>.  */

/* Written by Paul Eggert.  */

/* Although this module is not thread-safe, any races should be fairly
   rare and reasonably benign.  For complete thread-safety, use a C
   library with a working timezone_t type, so that this module is not
   needed.  */

#include <config.h>

#include <time.h>

#include <errno.h>
#include <limits.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>

#include "flexmember.h"
#include "time-internal.h"

#ifndef SIZE_MAX
# define SIZE_MAX ((size_t) -1)
#endif

/* The approximate size to use for small allocation requests.  This is
   the largest "small" request for the GNU C library malloc.  */
enum { DEFAULT_MXFAST = 64 * sizeof (size_t) / 4 };

/* Minimum size of the ABBRS member of struct tm_zone.  ABBRS is larger
   only in the unlikely case where an abbreviation longer than this is
   used.  */
enum { ABBR_SIZE_MIN = DEFAULT_MXFAST - offsetof (struct tm_zone, abbrs) };

/* Magic cookie timezone_t value, for local time.  It differs from
   NULL and from all other timezone_t values.  Only the address
   matters; the pointer is never dereferenced.  */
static timezone_t const local_tz = (timezone_t) 1;

#if HAVE_TM_ZONE || HAVE_TZNAME

/* Return true if the values A and B differ according to the rules for
   tm_isdst: A and B differ if one is zero and the other positive.  */
static bool
isdst_differ (int a, int b)
{
  return !a != !b && 0 <= a && 0 <= b;
}

/* Return true if A and B are equal.  */
static int
equal_tm (const struct tm *a, const struct tm *b)
{
  return ! ((a->tm_sec ^ b->tm_sec)
            | (a->tm_min ^ b->tm_min)
            | (a->tm_hour ^ b->tm_hour)
            | (a->tm_mday ^ b->tm_mday)
            | (a->tm_mon ^ b->tm_mon)
            | (a->tm_year ^ b->tm_year)
            | isdst_differ (a->tm_isdst, b->tm_isdst));
}

#endif

/* Copy to ABBRS the abbreviation at ABBR with size ABBR_SIZE (this
   includes its trailing null byte).  Append an extra null byte to
   mark the end of ABBRS.  */
static void
extend_abbrs (char *abbrs, char const *abbr, size_t abbr_size)
{
  memcpy (abbrs, abbr, abbr_size);
  abbrs[abbr_size] = '\0';
}

/* Return a newly allocated time zone for NAME, or NULL on failure.
   A null NAME stands for wall clock time (which is like unset TZ).  */
timezone_t
tzalloc (char const *name)
{
  size_t name_size = name ? strlen (name) + 1 : 0;
  size_t abbr_size = name_size < ABBR_SIZE_MIN ? ABBR_SIZE_MIN : name_size + 1;
  timezone_t tz = malloc (FLEXSIZEOF (struct tm_zone, abbrs, abbr_size));
  if (tz)
    {
      tz->next = NULL;
#if HAVE_TZNAME && !HAVE_TM_ZONE
      tz->tzname_copy[0] = tz->tzname_copy[1] = NULL;
#endif
      tz->tz_is_set = !!name;
      tz->abbrs[0] = '\0';
      if (name)
        extend_abbrs (tz->abbrs, name, name_size);
    }
  return tz;
}

/* Save into TZ any nontrivial time zone abbreviation used by TM, and
   update *TM (if HAVE_TM_ZONE) or *TZ (if !HAVE_TM_ZONE &&
   HAVE_TZNAME) if they use the abbreviation.  Return true if
   successful, false (setting errno) otherwise.  */
static bool
save_abbr (timezone_t tz, struct tm *tm)
{
#if HAVE_TM_ZONE || HAVE_TZNAME
  char const *zone = NULL;
  char *zone_copy = (char *) "";

# if HAVE_TZNAME
  int tzname_index = -1;
# endif

# if HAVE_TM_ZONE
  zone = tm->tm_zone;
# endif

# if HAVE_TZNAME
  if (! (zone && *zone) && 0 <= tm->tm_isdst)
    {
      tzname_index = tm->tm_isdst != 0;
      zone = tzname[tzname_index];
    }
# endif

  /* No need to replace null zones, or zones within the struct tm.  */
  if (!zone || ((char *) tm <= zone && zone < (char *) (tm + 1)))
    return true;

  if (*zone)
    {
      zone_copy = tz->abbrs;

      while (strcmp (zone_copy, zone) != 0)
        {
          if (! (*zone_copy || (zone_copy == tz->abbrs && tz->tz_is_set)))
            {
              size_t zone_size = strlen (zone) + 1;
              size_t zone_used = zone_copy - tz->abbrs;
              if (SIZE_MAX - zone_used < zone_size)
                {
                  errno = ENOMEM;
                  return false;
                }
              if (zone_used + zone_size < ABBR_SIZE_MIN)
                extend_abbrs (zone_copy, zone, zone_size);
              else
                {
                  tz = tz->next = tzalloc (zone);
                  if (!tz)
                    return false;
                  tz->tz_is_set = 0;
                  zone_copy = tz->abbrs;
                }
              break;
            }

          zone_copy += strlen (zone_copy) + 1;
          if (!*zone_copy && tz->next)
            {
              tz = tz->next;
              zone_copy = tz->abbrs;
            }
        }
    }

  /* Replace the zone name so that its lifetime matches that of TZ.  */
# if HAVE_TM_ZONE
  tm->tm_zone = zone_copy;
# else
  if (0 <= tzname_index)
    tz->tzname_copy[tzname_index] = zone_copy;
# endif
#endif

  return true;
}

/* Free a time zone.  */
void
tzfree (timezone_t tz)
{
  if (tz != local_tz)
    while (tz)
      {
        timezone_t next = tz->next;
        free (tz);
        tz = next;
      }
}

/* Get and set the TZ environment variable.  These functions can be
   overridden by programs like Emacs that manage their own environment.  */

#ifndef getenv_TZ
static char *
getenv_TZ (void)
{
  return getenv ("TZ");
}
#endif

#ifndef setenv_TZ
static int
setenv_TZ (char const *tz)
{
  return tz ? setenv ("TZ", tz, 1) : unsetenv ("TZ");
}
#endif

/* Change the environment to match the specified timezone_t value.
   Return true if successful, false (setting errno) otherwise.  */
static bool
change_env (timezone_t tz)
{
  if (setenv_TZ (tz->tz_is_set ? tz->abbrs : NULL) != 0)
    return false;
  tzset ();
  return true;
}

/* Temporarily set the time zone to TZ, which must not be null.
   Return LOCAL_TZ if the time zone setting is already correct.
   Otherwise return a newly allocated time zone representing the old
   setting, or NULL (setting errno) on failure.  */
static timezone_t
set_tz (timezone_t tz)
{
  char *env_tz = getenv_TZ ();
  if (env_tz
      ? tz->tz_is_set && strcmp (tz->abbrs, env_tz) == 0
      : !tz->tz_is_set)
    return local_tz;
  else
    {
      timezone_t old_tz = tzalloc (env_tz);
      if (!old_tz)
        return old_tz;
      if (! change_env (tz))
        {
          int saved_errno = errno;
          tzfree (old_tz);
          errno = saved_errno;
          return NULL;
        }
      return old_tz;
    }
}

/* Restore an old setting returned by set_tz.  It must not be null.
   Return true (preserving errno) if successful, false (setting errno)
   otherwise.  */
static bool
revert_tz (timezone_t tz)
{
  if (tz == local_tz)
    return true;
  else
    {
      int saved_errno = errno;
      bool ok = change_env (tz);
      if (!ok)
        saved_errno = errno;
      tzfree (tz);
      errno = saved_errno;
      return ok;
    }
}

/* Use time zone TZ to compute localtime_r (T, TM).  */
struct tm *
localtime_rz (timezone_t tz, time_t const *t, struct tm *tm)
{
#ifdef HAVE_LOCALTIME_INFLOOP_BUG
  /* The -67768038400665599 comes from:
     https://lists.gnu.org/r/bug-gnulib/2017-07/msg00142.html
     On affected platforms the greatest POSIX-compatible time_t value
     that could return nonnull is 67768036191766798 (when
     TZ="XXX24:59:59" it resolves to the year 2**31 - 1 + 1900, on
     12-31 at 23:59:59), so test for that too while we're in the
     neighborhood.  */
  if (! (-67768038400665599 <= *t && *t <= 67768036191766798))
    {
      errno = EOVERFLOW;
      return NULL;
    }
#endif

  if (!tz)
    return gmtime_r (t, tm);
  else
    {
      timezone_t old_tz = set_tz (tz);
      if (old_tz)
        {
          bool abbr_saved = localtime_r (t, tm) && save_abbr (tz, tm);
          if (revert_tz (old_tz) && abbr_saved)
            return tm;
        }
      return NULL;
    }
}

/* Use time zone TZ to compute mktime (TM).  */
time_t
mktime_z (timezone_t tz, struct tm *tm)
{
  if (!tz)
    return timegm (tm);
  else
    {
      timezone_t old_tz = set_tz (tz);
      if (old_tz)
        {
          time_t t = mktime (tm);
#if HAVE_TM_ZONE || HAVE_TZNAME
          time_t badtime = -1;
          struct tm tm_1;
          if ((t != badtime
               || (localtime_r (&t, &tm_1) && equal_tm (tm, &tm_1)))
              && !save_abbr (tz, tm))
            t = badtime;
#endif
          if (revert_tz (old_tz))
            return t;
        }
      return -1;
    }
}