Coverage Report

Created: 2024-09-08 06:20

/src/FreeRDP/winpr/libwinpr/timezone/TimeZoneNameMapUtils.c
Line
Count
Source (jump to first uncovered line)
1
/**
2
 * WinPR: Windows Portable Runtime
3
 * Time Zone Name Map Utils
4
 *
5
 * Copyright 2024 Armin Novak <anovak@thincast.com>
6
 * Copyright 2024 Thincast Technologies GmbH
7
 *
8
 * Licensed under the Apache License, Version 2.0 (the "License");
9
 * you may not use this file except in compliance with the License.
10
 * You may obtain a copy of the License at
11
 *
12
 *     http://www.apache.org/licenses/LICENSE-2.0
13
 *
14
 * Unless required by applicable law or agreed to in writing, software
15
 * distributed under the License is distributed on an "AS IS" BASIS,
16
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
 * See the License for the specific language governing permissions and
18
 * limitations under the License.
19
 */
20
#include <winpr/config.h>
21
#include <winpr/assert.h>
22
#include <winpr/string.h>
23
#include <winpr/synch.h>
24
#include <winpr/json.h>
25
#include <winpr/path.h>
26
#include <winpr/file.h>
27
28
#include "../log.h"
29
30
#include <string.h>
31
32
#define TAG WINPR_TAG("timezone.utils")
33
34
#if defined(WITH_TIMEZONE_ICU)
35
#include <unicode/ucal.h>
36
#else
37
#include "WindowsZones.h"
38
#endif
39
40
#include "TimeZoneNameMap.h"
41
42
typedef struct
43
{
44
  size_t count;
45
  TimeZoneNameMapEntry* entries;
46
} TimeZoneNameMapContext;
47
48
static TimeZoneNameMapContext tz_context = { 0 };
49
50
static void tz_entry_free(TimeZoneNameMapEntry* entry)
51
282
{
52
282
  if (!entry)
53
0
    return;
54
282
  free(entry->DaylightName);
55
282
  free(entry->DisplayName);
56
282
  free(entry->Iana);
57
282
  free(entry->Id);
58
282
  free(entry->StandardName);
59
60
282
  const TimeZoneNameMapEntry empty = { 0 };
61
282
  *entry = empty;
62
282
}
63
64
static TimeZoneNameMapEntry tz_entry_clone(const TimeZoneNameMapEntry* entry)
65
282
{
66
282
  TimeZoneNameMapEntry clone = { 0 };
67
282
  if (!entry)
68
0
    return clone;
69
70
282
  if (entry->DaylightName)
71
282
    clone.DaylightName = _strdup(entry->DaylightName);
72
282
  if (entry->DisplayName)
73
282
    clone.DisplayName = _strdup(entry->DisplayName);
74
282
  if (entry->Iana)
75
282
    clone.Iana = _strdup(entry->Iana);
76
282
  if (entry->Id)
77
282
    clone.Id = _strdup(entry->Id);
78
282
  if (entry->StandardName)
79
282
    clone.StandardName = _strdup(entry->StandardName);
80
282
  return clone;
81
282
}
82
83
static void tz_context_free(void)
84
2
{
85
284
  for (size_t x = 0; x < tz_context.count; x++)
86
282
    tz_entry_free(&tz_context.entries[x]);
87
2
  free(tz_context.entries);
88
2
  tz_context.count = 0;
89
2
  tz_context.entries = NULL;
90
2
}
91
92
#if defined(WITH_TIMEZONE_FROM_FILE) && defined(WITH_WINPR_JSON)
93
static char* tz_get_object_str(WINPR_JSON* json, size_t pos, const char* name)
94
{
95
  WINPR_ASSERT(json);
96
  if (!WINPR_JSON_IsObject(json) || !WINPR_JSON_HasObjectItem(json, name))
97
  {
98
    WLog_WARN(TAG, "Invalid JSON entry at entry %" PRIuz ", missing an Object named '%s'", pos,
99
              name);
100
    return NULL;
101
  }
102
  WINPR_JSON* obj = WINPR_JSON_GetObjectItem(json, name);
103
  WINPR_ASSERT(obj);
104
  if (!WINPR_JSON_IsString(obj))
105
  {
106
    WLog_WARN(TAG,
107
              "Invalid JSON entry at entry %" PRIuz ", Object named '%s': Not of type string",
108
              pos, name);
109
    return NULL;
110
  }
111
112
  const char* str = WINPR_JSON_GetStringValue(obj);
113
  if (!str)
114
  {
115
    WLog_WARN(TAG, "Invalid JSON entry at entry %" PRIuz ", Object named '%s': NULL string",
116
              pos, name);
117
    return NULL;
118
  }
119
120
  return _strdup(str);
121
}
122
123
static BOOL tz_parse_json_entry(WINPR_JSON* json, size_t pos, TimeZoneNameMapEntry* entry)
124
{
125
  WINPR_ASSERT(entry);
126
  if (!json || !WINPR_JSON_IsObject(json))
127
  {
128
    WLog_WARN(TAG, "Invalid JSON entry at entry %" PRIuz ", expected an array", pos);
129
    return FALSE;
130
  }
131
132
  entry->Id = tz_get_object_str(json, pos, "Id");
133
  entry->StandardName = tz_get_object_str(json, pos, "StandardName");
134
  entry->DisplayName = tz_get_object_str(json, pos, "DisplayName");
135
  entry->DaylightName = tz_get_object_str(json, pos, "DaylightName");
136
  entry->Iana = tz_get_object_str(json, pos, "Iana");
137
  if (!entry->Id || !entry->StandardName || !entry->DisplayName || !entry->DaylightName ||
138
      !entry->Iana)
139
  {
140
    tz_entry_free(entry);
141
    return FALSE;
142
  }
143
  return TRUE;
144
}
145
146
static WINPR_JSON* load_timezones_from_file(const char* filename)
147
{
148
  INT64 jstrlen = 0;
149
  char* jstr = NULL;
150
  WINPR_JSON* json = NULL;
151
  FILE* fp = winpr_fopen(filename, "r");
152
  if (!fp)
153
  {
154
    WLog_WARN(TAG, "Timezone resource file '%s' does not exist or is not readable", filename);
155
    return NULL;
156
  }
157
158
  if (_fseeki64(fp, 0, SEEK_END) < 0)
159
  {
160
    WLog_WARN(TAG, "Timezone resource file '%s' seek failed", filename);
161
    goto end;
162
  }
163
  jstrlen = _ftelli64(fp);
164
  if (jstrlen < 0)
165
  {
166
    WLog_WARN(TAG, "Timezone resource file '%s' invalid length %" PRId64, filename, jstrlen);
167
    goto end;
168
  }
169
  if (_fseeki64(fp, 0, SEEK_SET) < 0)
170
  {
171
    WLog_WARN(TAG, "Timezone resource file '%s' seek failed", filename);
172
    goto end;
173
  }
174
175
  jstr = calloc(jstrlen + 1, sizeof(char));
176
  if (!jstr)
177
  {
178
    WLog_WARN(TAG, "Timezone resource file '%s' failed to allocate buffer of size %" PRId64,
179
              filename, jstrlen);
180
    goto end;
181
  }
182
183
  if (fread(jstr, jstrlen, sizeof(char), fp) != 1)
184
  {
185
    WLog_WARN(TAG, "Timezone resource file '%s' failed to read buffer of size %" PRId64,
186
              filename, jstrlen);
187
    goto end;
188
  }
189
190
  json = WINPR_JSON_ParseWithLength(jstr, jstrlen);
191
  if (!json)
192
    WLog_WARN(TAG, "Timezone resource file '%s' is not a valid JSON file", filename);
193
end:
194
  fclose(fp);
195
  free(jstr);
196
  return json;
197
}
198
#endif
199
200
static BOOL reallocate_context(TimeZoneNameMapContext* context, size_t size_to_add)
201
2
{
202
2
  {
203
2
    TimeZoneNameMapEntry* tmp = realloc(context->entries, (context->count + size_to_add) *
204
2
                                                              sizeof(TimeZoneNameMapEntry));
205
2
    if (!tmp)
206
0
    {
207
0
      WLog_WARN(TAG,
208
0
                "Failed to reallocate TimeZoneNameMapEntry::entries to %" PRIuz " elements",
209
0
                context->count + size_to_add);
210
0
      return FALSE;
211
0
    }
212
2
    const size_t offset = context->count;
213
2
    context->entries = tmp;
214
2
    context->count += size_to_add;
215
216
2
    memset(&context->entries[offset], 0, size_to_add * sizeof(TimeZoneNameMapEntry));
217
2
  }
218
2
  return TRUE;
219
2
}
220
221
static BOOL CALLBACK load_timezones(PINIT_ONCE once, PVOID param, PVOID* pvcontext)
222
2
{
223
2
  TimeZoneNameMapContext* context = param;
224
2
  WINPR_ASSERT(context);
225
2
  WINPR_UNUSED(pvcontext);
226
2
  WINPR_UNUSED(once);
227
228
2
  const TimeZoneNameMapContext empty = { 0 };
229
2
  *context = empty;
230
231
#if defined(WITH_TIMEZONE_FROM_FILE) && defined(WITH_WINPR_JSON)
232
  {
233
    char* filename = GetCombinedPath(WINPR_RESOURCE_ROOT, "TimeZoneNameMap.json");
234
    if (!filename)
235
    {
236
      WLog_WARN(TAG, "Could not create WinPR timezone resource filename");
237
      goto end;
238
    }
239
240
    WINPR_JSON* json = load_timezones_from_file(filename);
241
    if (!json)
242
      goto end;
243
244
    if (!WINPR_JSON_IsObject(json))
245
    {
246
      WLog_WARN(TAG, "Invalid top level JSON type in file %s, expected an array", filename);
247
      goto end;
248
    }
249
250
    WINPR_JSON* obj = WINPR_JSON_GetObjectItem(json, "TimeZoneNameMap");
251
    if (!WINPR_JSON_IsArray(obj))
252
    {
253
      WLog_WARN(TAG, "Invalid top level JSON type in file %s, expected an array", filename);
254
      goto end;
255
    }
256
    const size_t count = WINPR_JSON_GetArraySize(obj);
257
    const size_t offset = context->count;
258
    if (!reallocate_context(context, count))
259
      goto end;
260
    for (size_t x = 0; x < count; x++)
261
    {
262
      WINPR_JSON* entry = WINPR_JSON_GetArrayItem(obj, x);
263
      if (!tz_parse_json_entry(entry, x, &context->entries[offset + x]))
264
        goto end;
265
    }
266
267
  end:
268
    free(filename);
269
    WINPR_JSON_Delete(json);
270
  }
271
#endif
272
273
2
#if defined(WITH_TIMEZONE_COMPILED)
274
2
  {
275
    // Do not expose these, only used internally.
276
2
    extern const TimeZoneNameMapEntry TimeZoneNameMap[];
277
2
    extern const size_t TimeZoneNameMapSize;
278
279
2
    const size_t offset = context->count;
280
2
    if (!reallocate_context(context, TimeZoneNameMapSize))
281
0
      return FALSE;
282
284
    for (size_t x = 0; x < TimeZoneNameMapSize; x++)
283
282
      context->entries[offset + x] = tz_entry_clone(&TimeZoneNameMap[x]);
284
2
  }
285
0
#endif
286
287
0
  (void)atexit(tz_context_free);
288
2
  return TRUE;
289
2
}
290
291
const TimeZoneNameMapEntry* TimeZoneGetAt(size_t index)
292
11.2M
{
293
11.2M
  static INIT_ONCE init_guard = INIT_ONCE_STATIC_INIT;
294
295
11.2M
  InitOnceExecuteOnce(&init_guard, load_timezones, &tz_context, NULL);
296
11.2M
  if (index >= tz_context.count)
297
0
    return NULL;
298
11.2M
  return &tz_context.entries[index];
299
11.2M
}
300
301
static const char* return_type(const TimeZoneNameMapEntry* entry, TimeZoneNameType type)
302
238k
{
303
238k
  WINPR_ASSERT(entry);
304
238k
  switch (type)
305
238k
  {
306
0
    case TIME_ZONE_NAME_IANA:
307
0
      return entry->Iana;
308
79.6k
    case TIME_ZONE_NAME_ID:
309
79.6k
      return entry->Id;
310
79.6k
    case TIME_ZONE_NAME_STANDARD:
311
79.6k
      return entry->StandardName;
312
0
    case TIME_ZONE_NAME_DISPLAY:
313
0
      return entry->DisplayName;
314
79.6k
    case TIME_ZONE_NAME_DAYLIGHT:
315
79.6k
      return entry->DaylightName;
316
0
    default:
317
0
      return NULL;
318
238k
  }
319
238k
}
320
321
static BOOL iana_cmp(const TimeZoneNameMapEntry* entry, const char* iana)
322
11.2M
{
323
11.2M
  if (!entry || !iana || !entry->Iana)
324
0
    return FALSE;
325
11.2M
  return strcmp(iana, entry->Iana) == 0;
326
11.2M
}
327
328
static BOOL id_cmp(const TimeZoneNameMapEntry* entry, const char* id)
329
0
{
330
0
  if (!entry || !id || !entry->Id)
331
0
    return FALSE;
332
0
  return strcmp(id, entry->Id) == 0;
333
0
}
334
335
static const char* get_for_type(const char* val, TimeZoneNameType type,
336
                                BOOL (*cmp)(const TimeZoneNameMapEntry*, const char*))
337
238k
{
338
238k
  WINPR_ASSERT(val);
339
238k
  WINPR_ASSERT(cmp);
340
341
238k
  size_t index = 0;
342
11.2M
  while (TRUE)
343
11.2M
  {
344
11.2M
    const TimeZoneNameMapEntry* entry = TimeZoneGetAt(index++);
345
11.2M
    if (!entry)
346
0
      return NULL;
347
11.2M
    if (cmp(entry, val))
348
238k
      return return_type(entry, type);
349
11.2M
  }
350
238k
}
351
352
#if defined(WITH_TIMEZONE_ICU)
353
static char* get_wzid_icu(const UChar* utzid, size_t utzid_len)
354
{
355
  char* res = NULL;
356
  UErrorCode error = U_ZERO_ERROR;
357
358
  int32_t rc = ucal_getWindowsTimeZoneID(utzid, utzid_len, NULL, 0, &error);
359
  if ((error == U_BUFFER_OVERFLOW_ERROR) && (rc > 0))
360
  {
361
    rc++; // make space for '\0'
362
    UChar* wzid = calloc((size_t)rc + 1, sizeof(UChar));
363
    if (wzid)
364
    {
365
      UErrorCode error2 = U_ZERO_ERROR;
366
      int32_t rc2 = ucal_getWindowsTimeZoneID(utzid, utzid_len, wzid, rc, &error2);
367
      if (U_SUCCESS(error2) && (rc2 > 0))
368
        res = ConvertWCharNToUtf8Alloc(wzid, (size_t)rc, NULL);
369
      free(wzid);
370
    }
371
  }
372
  return res;
373
}
374
375
static char* get(const char* iana)
376
{
377
  size_t utzid_len = 0;
378
  UChar* utzid = ConvertUtf8ToWCharAlloc(iana, &utzid_len);
379
  if (!utzid)
380
    return NULL;
381
382
  char* wzid = get_wzid_icu(utzid, utzid_len);
383
  free(utzid);
384
  return wzid;
385
}
386
387
static const char* map_fallback(const char* iana, TimeZoneNameType type)
388
{
389
  char* wzid = get(iana);
390
  if (!wzid)
391
    return NULL;
392
393
  const char* res = get_for_type(wzid, type, id_cmp);
394
  free(wzid);
395
  return res;
396
}
397
#else
398
static const char* map_fallback(const char* iana, TimeZoneNameType type)
399
0
{
400
0
  if (!iana)
401
0
    return NULL;
402
403
0
  for (size_t x = 0; x < WindowsZonesNrElements; x++)
404
0
  {
405
0
    const WINDOWS_TZID_ENTRY* const entry = &WindowsZones[x];
406
0
    if (strchr(entry->tzid, ' '))
407
0
    {
408
0
      const char* res = NULL;
409
0
      char* tzid = _strdup(entry->tzid);
410
0
      char* ctzid = tzid;
411
0
      while (tzid)
412
0
      {
413
0
        char* space = strchr(tzid, ' ');
414
0
        if (space)
415
0
          *space++ = '\0';
416
0
        if (strcmp(tzid, iana) == 0)
417
0
        {
418
0
          res = entry->windows;
419
0
          break;
420
0
        }
421
0
        tzid = space;
422
0
      }
423
0
      free(ctzid);
424
0
      if (res)
425
0
        return res;
426
0
    }
427
0
    else if (strcmp(entry->tzid, iana) == 0)
428
0
      return entry->windows;
429
0
  }
430
431
0
  return NULL;
432
0
}
433
#endif
434
435
const char* TimeZoneIanaToWindows(const char* iana, TimeZoneNameType type)
436
238k
{
437
238k
  if (!iana)
438
0
    return NULL;
439
440
238k
  const char* val = get_for_type(iana, type, iana_cmp);
441
238k
  if (val)
442
238k
    return val;
443
444
0
  const char* wzid = map_fallback(iana, type);
445
0
  if (!wzid)
446
0
    return NULL;
447
448
0
  return get_for_type(wzid, type, id_cmp);
449
0
}