Coverage Report

Created: 2025-07-11 06:29

/src/S2OPC/src/Common/helpers/sopc_date_time.c
Line
Count
Source (jump to first uncovered line)
1
/*
2
 * Licensed to Systerel under one or more contributor license
3
 * agreements. See the NOTICE file distributed with this work
4
 * for additional information regarding copyright ownership.
5
 * Systerel licenses this file to you under the Apache
6
 * License, Version 2.0 (the "License"); you may not use this
7
 * file except in compliance with the License. You may obtain
8
 * a copy of the License at
9
 *
10
 *   http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing,
13
 * software distributed under the License is distributed on an
14
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
 * KIND, either express or implied.  See the License for the
16
 * specific language governing permissions and limitations
17
 * under the License.
18
 */
19
20
#include <inttypes.h>
21
#include <limits.h>
22
#include <stdint.h>
23
#include <stdio.h>
24
#include <string.h>
25
26
#include "sopc_date_time.h"
27
28
#include "sopc_assert.h"
29
#include "sopc_helper_string.h"
30
#include "sopc_mem_alloc.h"
31
32
static const SOPC_DateTime SOPC_SECONDS_BETWEEN_EPOCHS = 11644473600;
33
static const SOPC_DateTime SOPC_SECOND_TO_100_NANOSECONDS = 10000000; // 10^7
34
35
char* SOPC_Time_GetString(SOPC_DateTime time, bool local, bool compact)
36
0
{
37
0
    static const size_t buf_size = 24;
38
39
0
    if (time == 0)
40
0
    {
41
0
        return NULL;
42
0
    }
43
44
0
    time_t seconds = 0;
45
0
    SOPC_ReturnStatus status = SOPC_Time_ToUnixTime(time, &seconds);
46
0
    SOPC_ASSERT(status == SOPC_STATUS_OK);
47
48
0
    uint32_t milliseconds = (uint32_t)((time / 10000) % 1000);
49
0
    struct tm tm;
50
51
0
    if (local)
52
0
    {
53
0
        status = SOPC_Time_Breakdown_Local(seconds, &tm);
54
0
    }
55
0
    else
56
0
    {
57
0
        status = SOPC_Time_Breakdown_UTC(seconds, &tm);
58
0
    }
59
60
0
    if (status != SOPC_STATUS_OK)
61
0
    {
62
0
        return NULL;
63
0
    }
64
65
0
    char* buf = SOPC_Calloc(buf_size, sizeof(char));
66
67
0
    if (buf == NULL)
68
0
    {
69
0
        return NULL;
70
0
    }
71
72
0
    size_t res = strftime(buf, buf_size - 1, compact ? "%Y%m%d_%H%M%S" : "%Y/%m/%d %H:%M:%S", &tm);
73
74
0
    if (res == 0)
75
0
    {
76
0
        SOPC_Free(buf);
77
0
        return NULL;
78
0
    }
79
80
0
    int res2 = snprintf(buf + res, 5, compact ? "_%03" PRIu32 : ".%03" PRIu32, milliseconds);
81
0
    SOPC_ASSERT(4 == res2);
82
83
0
    return buf;
84
0
}
85
86
static char* get_current_time_string(bool local, bool compact)
87
0
{
88
0
    return SOPC_Time_GetString(SOPC_Time_GetCurrentTimeUTC(), local, compact);
89
0
}
90
91
char* SOPC_Time_GetStringOfCurrentLocalTime(bool compact)
92
0
{
93
0
    return get_current_time_string(true, compact);
94
0
}
95
96
char* SOPC_Time_GetStringOfCurrentTimeUTC(bool compact)
97
0
{
98
0
    return get_current_time_string(false, compact);
99
0
}
100
101
SOPC_ReturnStatus SOPC_Time_FromUnixTime(SOPC_Unix_Time time, SOPC_DateTime* res)
102
0
{
103
0
    SOPC_ASSERT(time >= 0);
104
105
#if (SOPC_TIME_T_SIZE > 4)
106
    if (time > INT64_MAX)
107
    {
108
        return SOPC_STATUS_NOK;
109
    }
110
#endif
111
112
0
    int64_t dt = time;
113
114
0
    if (INT64_MAX - SOPC_SECONDS_BETWEEN_EPOCHS < dt)
115
0
    {
116
0
        return SOPC_STATUS_NOK;
117
0
    }
118
119
0
    dt += SOPC_SECONDS_BETWEEN_EPOCHS;
120
121
0
    if (INT64_MAX / SOPC_SECOND_TO_100_NANOSECONDS < dt)
122
0
    {
123
0
        return SOPC_STATUS_NOK;
124
0
    }
125
126
0
    dt *= SOPC_SECOND_TO_100_NANOSECONDS;
127
0
    *res = dt;
128
0
    return SOPC_STATUS_OK;
129
0
}
130
131
SOPC_ReturnStatus SOPC_Time_ToUnixTime(SOPC_DateTime dt, SOPC_Unix_Time* res)
132
0
{
133
0
    int64_t secs = dt / SOPC_SECOND_TO_100_NANOSECONDS;
134
135
0
    if (secs < SOPC_SECONDS_BETWEEN_EPOCHS)
136
0
    {
137
0
        return SOPC_STATUS_NOK;
138
0
    }
139
140
0
    secs -= SOPC_SECONDS_BETWEEN_EPOCHS;
141
142
    // Prevent using two memory words on 32-bit systems
143
0
    if (secs > LONG_MAX)
144
0
    {
145
0
        return SOPC_STATUS_NOK;
146
0
    }
147
148
0
    if (secs == (int64_t)(SOPC_Unix_Time) secs)
149
0
    {
150
0
        *res = (SOPC_Unix_Time) secs;
151
0
        return SOPC_STATUS_OK;
152
0
    }
153
0
    else
154
0
    {
155
0
        return SOPC_STATUS_NOK;
156
0
    }
157
0
}
158
159
static bool parseTwoDigitsUint8(const char* startPointer, size_t len, const char endChar, uint8_t* pOut)
160
0
{
161
0
    SOPC_ASSERT(NULL != startPointer);
162
0
    SOPC_ASSERT(NULL != pOut);
163
164
0
    if ((len > 2 && startPointer[2] != endChar) || len < 2)
165
0
    {
166
0
        return false;
167
0
    }
168
169
0
    SOPC_ReturnStatus status = SOPC_strtouint8_t(startPointer, pOut, 10, endChar);
170
0
    if (SOPC_STATUS_OK != status)
171
0
    {
172
0
        return false;
173
0
    }
174
175
0
    return true;
176
0
}
177
178
bool SOPC_tm_FromXsdDateTime(const char* datetime, size_t len, SOPC_tm* tm)
179
0
{
180
0
    if (NULL == tm)
181
0
    {
182
0
        return false;
183
0
    }
184
185
0
    SOPC_tm res;
186
0
    res.year = 0;
187
0
    res.month = 0;
188
0
    res.day = 0;
189
0
    res.hour = 0;
190
0
    res.minute = 0;
191
0
    res.second = 0;
192
0
    res.secondAndFrac = 0.0;
193
0
    res.UTC = true;
194
0
    res.UTC_neg_off = false;
195
0
    res.UTC_hour_off = 0;
196
0
    res.UTC_min_off = 0;
197
198
    // Check input: minimum length '<YYYY>-<MM>-<DD>T<hh>:<mm>:<ss>'
199
0
    if (NULL == datetime || len < 19)
200
0
    {
201
0
        return false;
202
0
    }
203
204
0
    const char* currentPointer = datetime;
205
0
    size_t remainingLength = len;
206
207
    /*
208
     * Parse year:
209
     * -?([1-9][0-9]{3,}|0[0-9]{3})-
210
     */
211
    // Check if year is prefixed by '-'
212
0
    const char* endPointer = NULL;
213
214
    // Manage the case of negative year by searching from next character
215
    // since at least 4 digits are expected in both cases
216
0
    endPointer = memchr(currentPointer + 1, '-', remainingLength - 1);
217
0
    if (NULL == endPointer || endPointer - currentPointer < (*currentPointer == '-' ? 5 : 4))
218
0
    {
219
        // End character not found or year < 4 digits year
220
0
        return false;
221
0
    }
222
223
0
    bool bres = SOPC_strtoint(currentPointer, (size_t)(endPointer - currentPointer), 16, &res.year);
224
0
    if (!bres)
225
0
    {
226
0
        return false;
227
0
    }
228
0
    endPointer++; // remove '-' end separator
229
0
    SOPC_ASSERT(endPointer > currentPointer);
230
0
    remainingLength -= (size_t)(endPointer - currentPointer);
231
0
    currentPointer = endPointer;
232
233
    /*
234
     * Parse Month:
235
     * (0[1-9]|1[0-2])-
236
     */
237
0
    bres = parseTwoDigitsUint8(currentPointer, remainingLength, '-', &res.month);
238
0
    if (!bres || res.month < 1 || res.month > 12)
239
0
    {
240
0
        return false;
241
0
    }
242
0
    remainingLength -= 3;
243
0
    currentPointer += 3;
244
245
    /*
246
     * Parse Day:
247
     * (0[1-9]|[12][0-9]|3[01])T
248
     */
249
0
    bres = parseTwoDigitsUint8(currentPointer, remainingLength, 'T', &res.day);
250
0
    if (!bres || res.day < 1 || res.day > 31)
251
0
    {
252
0
        return false;
253
0
    }
254
0
    remainingLength -= 3;
255
0
    currentPointer += 3;
256
257
    /*
258
     *  Check constraint: Day-of-month Values
259
     *  The day value must be no more than 30 if month is one of 4, 6, 9, or 11;
260
     *  no more than 28 if month is 2 and year is not divisible by 4,
261
     *  or is divisible by 100 but not by 400;
262
     *  and no more than 29 if month is 2 and year is divisible by 400, or by 4 but not by 100.
263
     */
264
0
    if (res.day > 30 && (4 == res.month || 6 == res.month || 9 == res.month || 11 == res.month))
265
0
    {
266
0
        return false;
267
0
    }
268
0
    if (res.day > 28 && (2 == res.month && (res.year % 4 != 0 || (0 == res.year % 100 && 0 != res.year % 400))))
269
0
    {
270
0
        return false;
271
0
    }
272
0
    if (res.day > 29 && 2 == res.month) // No more than 29 days in February
273
0
    {
274
0
        return false;
275
0
    }
276
277
    /*
278
     * Parse Hour:
279
     * ([01][0-9]|2[0-4]):
280
     */
281
0
    bres = parseTwoDigitsUint8(currentPointer, remainingLength, ':', &res.hour);
282
    // Note: accept hour = 24 for case 24:00:00.0 to be check after parsing minutes and seconds
283
0
    if (!bres || res.hour > 24)
284
0
    {
285
0
        return false;
286
0
    }
287
0
    remainingLength -= 3;
288
0
    currentPointer += 3;
289
290
    /*
291
     * Parse Minutes:
292
     * ([0-5][0-9]):
293
     */
294
0
    bres = parseTwoDigitsUint8(currentPointer, remainingLength, ':', &res.minute);
295
296
0
    if (!bres || res.minute > 59)
297
0
    {
298
0
        return false;
299
0
    }
300
0
    remainingLength -= 3;
301
0
    currentPointer += 3;
302
303
    /*
304
     * Parse Seconds (whole number part):
305
     * ([0-5][0-9]):
306
     */
307
0
    if (remainingLength < 2)
308
0
    {
309
0
        return false;
310
0
    }
311
    // Use SOPC_strtouint to allow string ending without '\0' and avoid access out of string memory bounds
312
0
    bres = SOPC_strtouint(currentPointer, 2, 8, &res.second);
313
0
    if (!bres || res.second > 59)
314
0
    {
315
0
        return false;
316
0
    }
317
318
    // Initialize seconds without fraction first (in case of no fraction present)
319
0
    res.secondAndFrac = (double) res.second;
320
321
    // Check 24:00:00 special case
322
0
    if (24 == res.hour && (0 != res.minute || 0 != res.second))
323
0
    {
324
0
        return false;
325
0
    }
326
327
0
    if (2 == remainingLength)
328
0
    {
329
        // Whole datetime string consume without any error
330
0
        *tm = res;
331
332
0
        return true;
333
0
    }
334
335
    /*
336
     * Parse Seconds with fraction (if applicable)
337
     * [0-5][0-9](\.[0-9]+)?
338
     */
339
340
    // Check if there is a fraction of second
341
0
    if ('.' == currentPointer[2])
342
0
    {
343
        // Search for end character starting from character after the '.'
344
0
        endPointer = &currentPointer[3];
345
0
        size_t localRemLength = remainingLength - 3;
346
347
0
        while (localRemLength > 0 && *endPointer >= '0' && *endPointer <= '9')
348
0
        {
349
            // Check all digits are 0 if hour was 24
350
0
            if (24 == res.hour && '0' != *endPointer)
351
0
            {
352
0
                return false;
353
0
            }
354
0
            endPointer++;
355
0
            localRemLength--;
356
0
        }
357
358
        // Parse the seconds with fraction as a double value
359
0
        bres = SOPC_strtodouble(currentPointer, (size_t)(endPointer - currentPointer), 64, &res.secondAndFrac);
360
        // Note: we do not need to check for actual value since we already controlled each digit individually
361
        // If something went wrong it is either due to SOPC_strtodouble or due to double representation
362
0
        if (!bres)
363
0
        {
364
0
            return false;
365
0
        }
366
0
        remainingLength -= (size_t)(endPointer - currentPointer);
367
0
        currentPointer = endPointer;
368
0
    }
369
0
    else
370
0
    {
371
0
        remainingLength -= 2;
372
0
        currentPointer += 2;
373
0
    }
374
375
0
    if (0 != remainingLength && 'Z' != *currentPointer)
376
0
    {
377
        // Parse offset sign
378
0
        if ('-' == *currentPointer)
379
0
        {
380
0
            res.UTC_neg_off = true;
381
0
        }
382
0
        else if ('+' != *currentPointer)
383
0
        {
384
0
            return false;
385
0
        }
386
387
0
        remainingLength--;
388
0
        currentPointer++;
389
390
        /*
391
         * Parse Hour Offset:
392
         * (0[0-9]|1[0-4]):
393
         */
394
0
        bres = parseTwoDigitsUint8(currentPointer, remainingLength, ':', &res.UTC_hour_off);
395
        // Note: accept hour = 14 for case 14:00 to be check after parsing minutes and seconds
396
0
        if (!bres || res.UTC_hour_off > 14)
397
0
        {
398
0
            return false;
399
0
        }
400
0
        remainingLength -= 3;
401
0
        currentPointer += 3;
402
403
        /*
404
         * Parse Minute Offset
405
         * ([0-5][0-9]):
406
         */
407
0
        if (remainingLength < 2)
408
0
        {
409
0
            return false;
410
0
        }
411
        // Use SOPC_strtouint to allow string ending without '\0' and avoid access out of string memory bounds
412
0
        bres = SOPC_strtouint(currentPointer, 2, 8, &res.UTC_min_off);
413
        // Check for special case of 14:00 which is maximum offset value
414
0
        if (!bres || res.UTC_min_off > 59 || (14 == res.UTC_hour_off && 0 != res.UTC_min_off))
415
0
        {
416
0
            return false;
417
0
        }
418
419
0
        remainingLength -= 2;
420
0
        currentPointer += 2;
421
422
        // Set UTC flag regarding offset
423
0
        res.UTC = (0 == res.UTC_hour_off && 0 == res.UTC_min_off);
424
0
    }
425
0
    else if ('Z' == *currentPointer)
426
0
    {
427
0
        remainingLength--;
428
0
        currentPointer++;
429
0
    }
430
431
0
    if (0 != remainingLength)
432
0
    {
433
0
        return false;
434
0
    }
435
436
0
    *tm = res;
437
0
    return true;
438
0
}
439
440
static int64_t daysSince1601(int16_t year, uint8_t month, uint8_t day)
441
0
{
442
0
    SOPC_ASSERT(year >= 1601);
443
0
    SOPC_ASSERT(year <= 10000);
444
445
    // Years since 1601
446
0
    int16_t elapsedYearsSince1601 = (int16_t)(year - 1601);
447
448
    // Month-to-day offset for non-leap-years.
449
0
    const int64_t monthDaysElapsed[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334};
450
451
    // Number of February months since 01/01/1601
452
0
    int64_t nbFebs = elapsedYearsSince1601 + (1 - (month <= 2 ? 1 : 0));
453
454
    // Total number of leap days since 01/01/1601
455
0
    int64_t leapDays = (nbFebs / 4) - (nbFebs / 100) + (nbFebs / 400);
456
457
    // Total number of days =
458
    // 365 * elapsed years + elapsed leap days + elapsed days in current year before current month (without leap day)
459
    // + elapsed days in current month (- 1 since current day not elapsed yet)
460
0
    int64_t days = 365 * elapsedYearsSince1601 + leapDays + monthDaysElapsed[month - 1] + day - 1;
461
462
0
    return days;
463
0
}
464
465
static int64_t secondsSince1601(int16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second)
466
0
{
467
0
    SOPC_ASSERT(year >= 1601 || (year == 1600 && month == 12 && day == 31));
468
0
    SOPC_ASSERT(year <= 10000);
469
470
0
    if (year >= 1601) // number of seconds since 1601
471
0
    {
472
0
        const int64_t secsByDay = 86400;
473
0
        const int64_t secsCurrentDay = hour * 3600 + minute * 60 + second;
474
0
        const int64_t nbDaysSince1601 = daysSince1601(year, month, day);
475
476
0
        return secsByDay * nbDaysSince1601 + secsCurrentDay;
477
0
    }
478
0
    else // number of seconds until 01/01/1601 (from day 31/12/1600)
479
0
    {
480
0
        const int64_t secsUntil1601 = (24 - hour) * 3600 - minute * 60 - second;
481
        // negative number of seconds since reference is 1601
482
0
        return -1 * secsUntil1601;
483
0
    }
484
0
}
485
486
SOPC_ReturnStatus SOPC_Time_FromXsdDateTime(const char* dateTime, size_t len, int64_t* res)
487
0
{
488
    // 100ns as a fraction of second
489
0
    const float sec_fraction_100ns = (float) 0.0000001;
490
491
0
    SOPC_tm tm_res;
492
0
    memset(&tm_res, 0, sizeof(tm_res));
493
494
0
    bool parseRes = SOPC_tm_FromXsdDateTime(dateTime, len, &tm_res);
495
496
0
    if (!parseRes)
497
0
    {
498
0
        return SOPC_STATUS_INVALID_PARAMETERS;
499
0
    }
500
501
0
    if (tm_res.year < 1601 && (tm_res.year != 1600 || tm_res.month != 12 || tm_res.day != 31))
502
0
    {
503
        // A date/time value is encoded as 0 if is equal to or earlier than 1601-01-01 12:00AM UTC.
504
        // Due to timezone offset to be considered, we keep a 24:00:00 margin as a first approximation.
505
        // It excludes any date earlier than 31-12-1600.
506
0
        *res = 0;
507
0
        return SOPC_STATUS_OK;
508
0
    }
509
0
    else if (tm_res.year > 9999 && (tm_res.year != 10000 || tm_res.month != 1 || tm_res.day != 1))
510
0
    {
511
        // A date/time is encoded as the maximum value for an Int64 if
512
        // the value is equal to or greater than 9999-12-31 11:59:59PM UTC
513
        // Due to timezone offset to be considered, we keep a 24:00:00 margin as a first approximation.
514
        // It excludes any date greater than 01-01-10000.
515
0
        *res = INT64_MAX;
516
0
        return SOPC_STATUS_OK;
517
0
    }
518
519
    // Compute seconds since 1601
520
0
    int64_t secsSince1601 =
521
0
        secondsSince1601(tm_res.year, tm_res.month, tm_res.day, tm_res.hour, tm_res.minute, tm_res.second);
522
523
0
    if (!tm_res.UTC)
524
0
    {
525
0
        int64_t offset = tm_res.UTC_hour_off * 3600 + tm_res.UTC_min_off * 60;
526
0
        if (tm_res.UTC_neg_off)
527
0
        {
528
            // Negative offset, add the offset
529
0
            secsSince1601 += offset;
530
0
        }
531
0
        else
532
0
        {
533
            // Positive offset, substract the offset
534
0
            secsSince1601 -= offset;
535
0
        }
536
0
    }
537
538
    // Check date >= 1601-01-01 12:00AM UTC
539
0
    if (secsSince1601 < 0)
540
0
    {
541
        // A date/time value is encoded as 0 if is equal to or earlier than 1601-01-01 12:00AM UTC.
542
        // Due to timezone offset to be considered, we keep a 24:00:00 margin as a first approximation.
543
        // It excludes any date earlier than 31-12-1600.
544
0
        *res = 0;
545
0
        return SOPC_STATUS_OK;
546
0
    }
547
0
    else if (secsSince1601 >= 265046774399) // Check date < 9999-12-31 11:59:59PM UTC
548
0
    {
549
        // Note: 265046774399 == secondsSince1601(9999, 12, 31, 23, 59, 59)
550
551
        // A date/time is encoded as the maximum value for an Int64 if
552
        // the value is equal to or greater than 9999-12-31 11:59:59PM UTC
553
0
        *res = INT64_MAX;
554
0
        return SOPC_STATUS_OK;
555
0
    }
556
557
    // Compute seconds fraction if significant
558
0
    double sec_fraction = tm_res.secondAndFrac - (double) tm_res.second;
559
0
    int64_t hundredOfNanoseconds = (int64_t)(sec_fraction / (double) sec_fraction_100ns);
560
561
    // Note: no overflow possible for 1601 <= year <= 10000
562
0
    *res = secsSince1601 * SOPC_SECOND_TO_100_NANOSECONDS + hundredOfNanoseconds;
563
0
    return SOPC_STATUS_OK;
564
0
}